Stream motion data from glasses sensors

Updated: Sep 18, 2026

Overview

The Motion capability streams inertial measurement unit (IMU) samples from the wearer’s glasses to your app. Use it for head-orientation input, gesture and activity detection, image stabilization hints, or to align sensor data with a camera stream.
Beta: Motion is a beta capability. It is available in the development and beta release channels only, and its API may change before general availability.
The glasses emit a MotionSample each time a new accelerometer reading arrives. The sample carries that reading’s timestamp together with the most recent gyroscope, magnetometer, and orientation values, which may be slightly older:
FieldTypeUnitNotes
timestampNs
64-bit integer
Nanoseconds
Device monotonic clock. Use it for ordering and for computing intervals, not as a wall-clock time.
accelerometer
3-component vector
m/s²
Includes gravity. A device at rest reads approximately 9.81 on the axis opposing gravity. Can be null.
gyroscope
3-component vector
rad/s
Angular velocity. Can be null.
magnetometer
3-component vector
µT
Not every device produces it, so this value can be null.
orientation
Quaternion
—
Fused orientation, with w as the scalar component. Not every device produces it, so this value can be null.
source
Enum
—
Which sensor package produced the sample: glasses, Meta Neural Band, or an unknown device.
timestampNs and source are always present. All four sensor readings are independently nullable: check each one you use, and do not discard a whole sample because a different reading is missing.
A sample streamed from glasses carries an accelerometer reading in practice, since a new accelerometer reading is what triggers it. Treat that as an observation rather than a guarantee — the field is optional in the SDK type, a sample replayed from a Mock Device Kit feed can omit it, and other sources need not behave the same way.

Use this page with AI

Copy this prompt into your AI coding tool to add or debug a Motion integration:
Use https://wearables.developer.meta.com/docs/develop/dat/motion/, then use the Wearables MCP endpoint https://mcp.developer.meta.com/wearables to call search_dat_docs for current Device Access Toolkit Motion guidance. Inspect my code first, then add the smallest working slice: add the Motion capability to a started device session, start it, consume the sample stream, and observe capability state and errors. Handle null accelerometer, gyroscope, magnetometer, and orientation readings, pick a sampling rate that matches what my feature actually needs, and remove the capability during teardown.

Prerequisites

  • A working integration built from iOS integrations or Android integrations, including registration with the Meta AI app.
  • A device session in the started state. You cannot add Motion to a session that has not started.
Motion does not require a user-facing permission request. Unlike camera and microphone access, there is no Permission value to check or request for motion data, so you do not call checkPermissionStatus or requestPermission before using it. See Permissions and registration for the permissions that do require a user prompt.

Choose a sampling rate

Request a rate with MotionConfiguration. Higher rates cost more battery on the glasses and more bandwidth on the link to the phone, so choose the lowest rate that satisfies your feature.
RateTypical use
5 Hz
Coarse activity or posture checks. Lowest power.
10 Hz
Default. Battery-conscious general use.
15 Hz
Smoother orientation tracking.
24 Hz
Matching a nominal 24 fps cadence.
30 Hz
Responsive head-motion input.
60 Hz
Highest fidelity. Highest power cost.
If you do not pass a configuration, the capability uses 10 Hz.
The rate you request is a target, not a guarantee. Compute intervals from timestampNs instead of assuming a fixed cadence.

Capability lifecycle

Adding Motion to a session does not start it. Call start() explicitly, then observe state to know when samples will begin.
StateMeaning
stopped
Not streaming. This is the initial state and the resting state after stop().
starting
start() was called and the device is bringing the sensors up.
started
Samples are flowing.
stopping
Teardown is in progress.
paused
Android only. The device session paused, so streaming is suspended.
On Android, if the device session pauses while Motion is starting or started, the capability reports paused and suspends streaming. When the session resumes, it restarts automatically and moves back through starting to started, so you do not call start() yourself.
A start() call made while the capability is starting, started, or stopping is ignored, so the capability must be back at stopped before you start it again. The two platforms get there differently: on Android stop() completes synchronously and state is already stopped when the call returns, whereas on iOS stop() requests the transition and the capability passes through stopping asynchronously. Observe state rather than assuming the transition has finished if you intend to restart on iOS.
On iOS, if the capability starts but no sample arrives within 10 seconds, it reports a sensor-unavailable error and stops. Android has no start timeout: the capability stays in starting until a sample arrives or an error ends the attempt.
The sample stream buffers the most recent 256 samples and drops the oldest when a consumer falls behind. Keep per-sample work small, and hand heavy processing off to another worker so you do not lose data.

Error handling

Three errors can occur while Motion is active:
ErrorMeaningRecovery
Sensor unavailable
The glasses could not bring the motion sensors up, or, on iOS, no sample arrived before the start timeout.
Ends the current start attempt, not the capability. Once state returns to stopped, you can call start() again.
Capability closed
You used a capability that had already been removed, or whose session had ended.
Terminal for that instance. Add Motion again to a started session.
Device disconnected
The glasses disconnected while the capability was starting or streaming.
On Android the capability restarts automatically when the session reconnects, so do not add it again yourself. On iOS, reattach once you have a started session.
For the enumerated values behind this table, see MotionError in the API reference for Android and iOS.

Android

Step 1: Add the dependency

Add the motion artifact alongside the core artifact in your version catalog, using the same version reference as the rest of the SDK.
[libraries]
mwdat-core = { group = "com.meta.wearable", name = "mwdat-core", version.ref = "mwdat" }
mwdat-motion = { group = "com.meta.wearable", name = "mwdat-motion", version.ref = "mwdat" }
dependencies {
  implementation(libs.mwdat.core)
  implementation(libs.mwdat.motion)
}

Step 2: Add the capability to a started session

Call addMotion on a started DeviceSession. It returns a result, so handle the failure branch rather than assuming success.
import com.meta.wearable.dat.motion.Motion
import com.meta.wearable.dat.motion.addMotion
import com.meta.wearable.dat.motion.types.MotionConfiguration
import com.meta.wearable.dat.motion.types.MotionSamplingRate

val configuration = MotionConfiguration(samplingRate = MotionSamplingRate.HZ_30)

val motion: Motion =
  session
    .addMotion(configuration)
    .onFailure { error, _ -> showError(error.description) }
    .getOrNull() ?: return
Use onFailure, fold, or errorOrNull() to reach the typed DeviceSessionError. The single-argument getOrElse hands you the underlying Throwable instead, which has no description.

Step 3: Start streaming and collect samples

Collect samples, state, and errors on jobs that you keep references to and cancel during teardown.
import com.meta.wearable.dat.motion.types.MotionState
import kotlinx.coroutines.CoroutineStart

val stateJob = scope.launch {
  motion.state.collect { state ->
    if (state == MotionState.STARTED) onStreaming() else onNotStreaming()
  }
}

// `samples` does not replay, so subscribe before `start()` rather than racing it.
val samplesJob = scope.launch(start = CoroutineStart.UNDISPATCHED) {
  motion.samples.collect { sample ->
    sample.accelerometer?.let { onAcceleration(it.x, it.y, it.z) }
    sample.orientation?.let { onOrientation(it.x, it.y, it.z, it.w) }
  }
}

motion.start()
Handle each reading on its own. A sample with no accelerometer can still carry a usable orientation, so skipping the whole sample throws away data.
state and errors are state flows, so a new collector immediately receives the current value. samples is a shared flow with no replay, which is why the example subscribes with CoroutineStart.UNDISPATCHED before calling start(); a plain scope.launch may not be collecting yet when the first samples arrive.
errors holds null until something goes wrong, keeps the last error while the capability is stopped, and clears back to null when you call start() again.
import com.meta.wearable.dat.motion.types.MotionError

val errorsJob = scope.launch {
  motion.errors.collect { error ->
    when (error) {
      null -> Unit
      MotionError.SENSOR_UNAVAILABLE -> showError(error.description)
      MotionError.DEVICE_DISCONNECTED -> showReconnecting()
      MotionError.CAPABILITY_CLOSED -> scheduleReattach()
    }
  }
}
Each value recovers differently. See Error handling.

Step 4: Stop and remove the capability

import com.meta.wearable.dat.motion.removeMotion

motion.stop()

stateJob.cancel()
samplesJob.cancel()
errorsJob.cancel()

session.removeMotion().onFailure { error, _ -> showError(error.description) }
Cancel the collector jobs you started in Step 3; the scope does not do it for you unless you cancel the scope itself. removeMotion() returns a DatResult, so handle its failure branch rather than assuming teardown succeeded.
Cancelling the collectors immediately after stop() is safe here because Android’s stop() is synchronous: state has already reached stopped by the time it returns, so nothing is lost by stopping observation at this point. This is a terminal teardown — if instead you are stopping with the intention of starting again, keep the state collector running and wait for MotionState.STOPPED before calling start().
Removing the capability releases the sensors on the glasses. Do it as soon as your feature no longer needs motion data, rather than waiting for the session to end.

Test without hardware

Mock Device Kit can replay a deterministic recording through the same path as a real device, either from an in-memory list of samples or from a CSV file.
mockDevice.services.motion.setMotionFeed(samples = recordedSamples, loop = true)
loop is available only on the in-memory overload. The CSV overload, setMotionFeed(fileUri), plays the recording through once.
On Android you can set a feed before or during a session. Setting one while the capability is already streaming hot-swaps the recording and restarts replay from its first sample.
Replay follows the sampling rate from MotionConfiguration rather than the intervals between recorded timestamps.
A CSV recording has no header row and requires exactly 14 comma-separated numeric values per row, in this order: timestampNs,accelX,accelY,accelZ,gyroX,gyroY,gyroZ,magX,magY,magZ,orientationW,orientationX,orientationY,orientationZ. Blank lines and lines beginning with # are skipped. Because every column is required, a CSV recording cannot express an absent reading — use the in-memory overload to replay samples with null values.
Replayed samples always report glasses as their source, whatever the recording contains, so you cannot exercise Meta Neural Band or unknown-source handling through a feed.
See Android testing for how to create a mock device.

iOS

Step 1: Import the module

The motion module ships in the SDK Swift package you added in iOS integrations, but adding a package reference does not link every product in it. Add the MWDATMotion product to your app target alongside MWDATCore, at the same SDK version, then import it wherever you use the capability.
import MWDATCore
import MWDATMotion

Step 2: Add the capability to a started session

addMotion returns nil if the session has not started, and throws if a Motion capability is already attached.
let configuration = MotionConfiguration(samplingRate: .hz30)
guard let motion = try session.addMotion(configuration: configuration) else { return }

Step 3: Start streaming and consume samples

samples is an AsyncStream, so iterate it in a task that you retain and cancel during teardown. Observe state and errors through their publishers.
Store the listener tokens as properties, or keep them in a ListenerTokenBag. An AnyListenerToken cancels its subscription once nothing references it, so a token held only in a local constant stops observing as soon as setup returns.
// Properties on the type that owns the capability, not local constants.
private var stateToken: AnyListenerToken?
private var errorToken: AnyListenerToken?
private var samplesTask: Task<Void, Never>?

stateToken = motion.statePublisher.listen { state in
  Task { @MainActor in
    // Update your UI for .starting, .started, .stopping, or .stopped
  }
}

errorToken = motion.errorPublisher.listen { error in
  Task { @MainActor in
    switch error {
    case .sensorUnavailable:
      showError(error)
    case .capabilityClosed, .deviceDisconnected:
      scheduleReattach()
    @unknown default:
      showError(error)
    }
  }
}

let samples = motion.samples
samplesTask = Task {
  for await sample in samples {
    if let acceleration = sample.accelerometer {
      await onAcceleration(acceleration.simd)
    }

    if let orientation = sample.orientation {
      await onOrientation(orientation.simd)
    }
  }
}

motion.start()
MotionError is a public, non-frozen enum, so include an @unknown default branch. Without it the compiler warns, and a switch that does not handle a case added in a later beta traps at runtime.
Handle each reading on its own: a sample with no accelerometer can still carry a usable orientation.
Vector3 and Quaternion expose a simd property, so you can hand readings straight to SIMD3<Float> and simd_quatf math without converting component by component.
Neither statePublisher nor errorPublisher replays to a new listener; both deliver only events that occur after you subscribe. Attach both listeners before calling start() so you do not miss the first transition or an early failure.

Step 4: Stop and remove the capability

samplesTask?.cancel()
stateToken = nil
errorToken = nil
motion.stop()
try session.removeMotion()
Releasing the tokens cancels the state and error subscriptions.

Test without hardware

Mock Device Kit replays a recording from an array of samples or from a CSV file using the same format as Android. Use mockMotionGravityMetersPerSecondSquared for the at-rest gravity reading when you synthesize samples.
mockDevice.services.motion.setMotionFeed(recordedSamples)
Stage the feed before the consumer starts. Unlike Android, iOS ignores setMotionFeed while a Motion session is streaming, so a feed set mid-session has no effect.
The array takes MWDATMockDevice.MotionSample, which is a distinct type from the MWDATMotion.MotionSample you receive from the live stream. The iOS in-memory overload has no loop parameter.
As on Android, replay follows the configured sampling rate, a CSV recording has no header row and uses exactly 14 numeric columns per row, and replayed samples always report glasses as their source.
See iOS testing for how to create a mock device.

Platform differences

Key differences include:
  • Paused state. Android reports a PAUSED state when the device session pauses. iOS has no equivalent case, so on iOS a pause surfaces through the session state rather than through the capability.
  • Start timeout. iOS fails a start attempt with a sensor-unavailable error if no sample arrives within 10 seconds. Android has no start timeout.
  • State and error delivery. Android exposes state and errors as state flows, so a new collector immediately sees the current value, and errors clears to null on the next start(). On iOS both statePublisher and errorPublisher are non-replaying, so a listener attached after an event never sees it.
  • Sample stream. Android’s samples is a shared flow that supports multiple collectors and does not replay. iOS’s samples is a single-consumer AsyncStream that stays open across stop() and finishes only when the capability is removed or its session tears down.
  • Automatic restart. On Android a disconnect marks the capability for automatic restart when the session reconnects. On iOS you reattach it yourself.

Next steps