Help us improve
Share bugs, ideas, or general feedback.
From mwdat-android
Sets up display capability for Meta Ray-Ban Display glasses: device selection, UI DSL, icons, buttons, images, and video playback. Use with getting-started and permissions-registration for a full app.
npx claudepluginhub facebook/meta-wearables-dat-android --plugin mwdat-androidHow this skill is triggered — by the user, by Claude, or both
Slash command
/mwdat-android:display-accessThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use `mwdat-display` to render content on Meta Ray-Ban Display glasses.
Configures display-capable device selection, UI DSL, icons, buttons, images, and video playback for Meta Ray-Ban Display glasses using MWDATDisplay.
Adds interactive UI components (screens, buttons, lists, cards, forms, toggles, counters, nav bars) to Meta Display Glasses webapps (vanilla JS or React) with D-pad navigation, fixed viewport, and dark theme constraints.
Builds UI for Even Hub G2 glasses displays using text containers, lists, images, page lifecycle, and layout patterns on 576x288 greyscale canvas. For creating or updating glasses app content.
Share bugs, ideas, or general feedback.
Use mwdat-display to render content on Meta Ray-Ban Display glasses.
Use this skill with getting-started and permissions-registration when creating a full app. A Display app still needs SDK initialization, app registration, Android permissions, and DAT manifest metadata before it can create a session.
In libs.versions.toml:
[libraries]
mwdat-display = { group = "com.meta.wearable", name = "mwdat-display", version.ref = "mwdat" }
In app/build.gradle.kts:
dependencies {
implementation(libs.mwdat.core)
implementation(libs.mwdat.display)
}
Display apps need the same core DAT setup as other apps plus DAM enabled:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" />
<application ...>
<meta-data
android:name="com.meta.wearable.mwdat.APPLICATION_ID"
android:value="${mwdat_application_id}" />
<meta-data
android:name="com.meta.wearable.mwdat.CLIENT_TOKEN"
android:value="${mwdat_client_token}" />
<meta-data
android:name="com.meta.wearable.mwdat.DAM_ENABLED"
android:value="true" />
</application>
Set mwdat_application_id and mwdat_client_token from Gradle manifest placeholders or local.properties, as in the DisplayAccess sample. Developer Mode builds can use the developer registration flow, but production builds need real Wearables Developer Center credentials.
Request the runtime permissions before initializing DAT, then call Wearables.initialize(context) once and start observing SDK state. Start registration with Wearables.startRegistration(activity), collect Wearables.registrationState, collect Wearables.registrationErrorStream, and wait for RegistrationState.REGISTERED before creating a display session. Use Wearables.startUnregistration(activity) when the user disconnects the app.
Display content only works on connected, compatible devices whose type supports display. Use the public device filter when you want automatic selection:
import com.meta.wearable.dat.core.Wearables
import com.meta.wearable.dat.core.selectors.AutoDeviceSelector
val selector = AutoDeviceSelector(filter = { device -> device.isDisplayCapable() })
val sessionResult = Wearables.createSession(selector)
Use SpecificDeviceSelector(selectedDeviceId) instead when your UI lets the user pick a specific DeviceIdentifier from Wearables.devices.
For device picker UI, mirror the DisplayAccess sample: collect Wearables.devices, start a metadata collection for each device ID from Wearables.devicesMetadata[id], remove metadata for devices that disappear, and show device name, device.deviceType.description, device.linkState, device.compatibility, and device.isDisplayCapable(). Keep selection disabled unless the device is LinkState.CONNECTED and display-capable, and surface DeviceCompatibility.DEVICE_UPDATE_REQUIRED with an openFirmwareUpdate action.
import com.meta.wearable.dat.core.types.Device
import com.meta.wearable.dat.core.types.DeviceIdentifier
import com.meta.wearable.dat.core.types.LinkState
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
private val metadataJobs = mutableMapOf<DeviceIdentifier, Job>()
private val devicesMetadata = MutableStateFlow<Map<DeviceIdentifier, Device>>(emptyMap())
lifecycleScope.launch {
Wearables.devices.collect { deviceIds ->
(metadataJobs.keys - deviceIds).forEach { removedId ->
metadataJobs.remove(removedId)?.cancel()
devicesMetadata.update { current -> current - removedId }
}
deviceIds.forEach { id ->
metadataJobs.getOrPut(id) {
launch {
Wearables.devicesMetadata[id]?.collect { device ->
devicesMetadata.update { current -> current + (id to device) }
updateDeviceRow(
id = id,
name = device.name,
type = device.deviceType.description,
isConnected = device.linkState == LinkState.CONNECTED,
isDisplayCapable = device.isDisplayCapable(),
compatibility = device.compatibility,
)
}
}
}
}
}
}
If Wearables.createSession(...) fails with DeviceSessionError.DAT_APP_ON_THE_GLASSES_UPDATE_REQUIRED, show a DAT glasses app update action that calls Wearables.openDATGlassesAppUpdate(activity).
Add the Display capability only after the DeviceSession reaches DeviceSessionState.STARTED. Keep the sample app pattern of showing a "preparing" state after session start and enabling user content only once the Display capability reports DisplayState.STARTED.
import androidx.lifecycle.lifecycleScope
import com.meta.wearable.dat.core.Wearables
import com.meta.wearable.dat.core.types.DeviceIdentifier
import com.meta.wearable.dat.core.types.DeviceSessionError
import com.meta.wearable.dat.core.types.RegistrationState
import com.meta.wearable.dat.core.session.DeviceSession
import com.meta.wearable.dat.core.session.DeviceSessionState
import com.meta.wearable.dat.core.selectors.SpecificDeviceSelector
import com.meta.wearable.dat.display.Display
import com.meta.wearable.dat.display.addDisplay
import com.meta.wearable.dat.display.removeDisplay
import com.meta.wearable.dat.display.types.DisplayState
import kotlinx.coroutines.launch
private var display: Display? = null
fun startDisplaySession(selectedDeviceId: DeviceIdentifier) {
if (Wearables.registrationState.value != RegistrationState.REGISTERED) {
showError("Register with Meta AI before starting Display")
return
}
Wearables.createSession(SpecificDeviceSelector(selectedDeviceId))
.fold(
onSuccess = { session ->
lifecycleScope.launch {
session.errors.collect { error ->
if (error == DeviceSessionError.DAT_APP_ON_THE_GLASSES_UPDATE_REQUIRED) {
showDatAppUpdateAction()
}
showError(error.description)
}
}
lifecycleScope.launch {
session.state.collect { state ->
if (state == DeviceSessionState.STARTED && display == null) {
attachDisplay(session)
}
}
}
session.start()
},
onFailure = { error, _ ->
if (error == DeviceSessionError.DAT_APP_ON_THE_GLASSES_UPDATE_REQUIRED) {
showDatAppUpdateAction()
}
showError(error.description)
},
)
}
private fun attachDisplay(session: DeviceSession) {
session.addDisplay()
.fold(
onSuccess = { newDisplay ->
display = newDisplay
lifecycleScope.launch {
newDisplay.state.collect { state ->
if (state == DisplayState.STARTED) {
setTryItEnabled(true)
} else {
setTryItEnabled(false)
}
}
}
},
onFailure = { error, _ ->
showError(error.description)
},
)
}
fun stopDisplaySession(session: DeviceSession) {
session.removeDisplay().onFailure { error, _ -> showError(error.description) }
session.stop()
display = null
}
Build exactly one root view per sendContent call. Each call replaces the previous content on the glasses. Use a root flexBox { ... } for UI, or a root video(player = player) for video; do not put video(...) inside a flexBox.
Button and clickable flexBox callbacks are routed back to the phone app. Keep callbacks fast and delegate to your ViewModel to send the next content screen, as the DisplayAccess sample does for list items, Back, Start, Previous, Next, Done, and Watch video buttons.
import com.meta.wearable.dat.display.views.ButtonStyle
import com.meta.wearable.dat.display.views.FlexBoxBackground
import com.meta.wearable.dat.display.views.IconName
import com.meta.wearable.dat.display.views.TextColor
import com.meta.wearable.dat.display.views.TextStyle
private suspend fun sendStatusCard(display: Display) {
display.sendContent {
flexBox(
gap = 12,
padding = 24,
background = FlexBoxBackground.CARD,
onClick = { showDetailState() },
) {
text("Bike ride", style = TextStyle.HEADING)
text("Turn right in 200 ft", style = TextStyle.BODY, color = TextColor.SECONDARY)
button(
label = "Done",
style = ButtonStyle.PRIMARY,
iconName = IconName.CHECKMARK,
onClick = { showDoneState() },
)
}
}.onFailure { error, _ ->
showError(error.description)
}
}
Use HTTPS image URLs for image content, and use IconName enum values for built-in icons. Do not invent string icon names.
import com.meta.wearable.dat.display.views.CornerRadius
import com.meta.wearable.dat.display.views.IconStyle
import com.meta.wearable.dat.display.views.ImageSize
display.sendContent {
flexBox(gap = 8, padding = 24) {
image(
uri = "https://example.com/thumbnail.png",
sizePreset = ImageSize.FILL,
cornerRadius = CornerRadius.MEDIUM,
)
icon(name = IconName.GEAR, style = IconStyle.FILLED)
text("Device settings", style = TextStyle.BODY)
}
}
For URL-based video, create a VideoPlayer, send it as the root content, then call play() after the send succeeds. Collect both player.state and player.error; on VideoPlayerState.ENDED, cancel the video observer and send the next display screen. Use an HTTP or HTTPS VideoSource.Url, VideoCodec.MP4 for MP4 assets, and a video sized within the public player limits.
import com.meta.wearable.dat.display.types.VideoCodec
import com.meta.wearable.dat.display.types.VideoPlayerState
import com.meta.wearable.dat.display.types.VideoSource
import com.meta.wearable.dat.display.views.VideoPlayer
import kotlinx.coroutines.Job
private var videoStateJob: Job? = null
private var videoErrorJob: Job? = null
lifecycleScope.launch {
val currentDisplay = display ?: return@launch
val player = VideoPlayer(
source = VideoSource.Url("https://example.com/tutorial.mp4"),
codec = VideoCodec.MP4,
)
videoStateJob?.cancel()
videoErrorJob?.cancel()
videoStateJob = launch {
player.state.collect { state ->
if (state == VideoPlayerState.ENDED) {
videoStateJob?.cancel()
videoStateJob = null
sendStatusCard(currentDisplay)
}
}
}
videoErrorJob = launch {
player.error.collect { error ->
if (error != null) {
showError(error.description)
}
}
}
currentDisplay.sendContent { video(player = player) }
.onSuccess { player.play() }
.onFailure { error, _ -> showError(error.description) }
}
Wearables.registrationErrorStream, session.errors, Display.sendContent failures, and VideoPlayer.error; DeviceSession.start() returns Unit and reports async failures through session.errors.SpecificDeviceSelector for a user-picked device and AutoDeviceSelector(filter = { it.isDisplayCapable() }) only when automatic selection is acceptable.DeviceSessionState.STARTED before calling session.addDisplay().DisplayState.STARTED before enabling or sending user-triggered content.DeviceCompatibility.DEVICE_UPDATE_REQUIRED, offer Wearables.openFirmwareUpdate(activity).session.errors reports DAT_APP_ON_THE_GLASSES_UPDATE_REQUIRED, offer Wearables.openDATGlassesAppUpdate(activity).VideoPlayer instances, detach with session.removeDisplay(), and then stop the session when the display experience ends.Use the Display Access sample app for a complete flow: registration, device selection, display attachment, interactive content, and video.