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:| Field | Type | Unit | Notes |
|---|---|---|---|
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.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.
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.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.| Rate | Typical 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. |
timestampNs instead of assuming a fixed cadence.start() explicitly, then observe state to know when samples will begin.| State | Meaning |
|---|---|
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. |
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.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.starting until a sample arrives or an error ends the attempt.| Error | Meaning | Recovery |
|---|---|---|
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. |
MotionError in the API reference for Android and iOS.[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)
}
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
onFailure, fold, or errorOrNull() to reach the typed DeviceSessionError. The single-argument getOrElse hands you the underlying Throwable instead, which has no description.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()
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()
}
}
}
import com.meta.wearable.dat.motion.removeMotion
motion.stop()
stateJob.cancel()
samplesJob.cancel()
errorsJob.cancel()
session.removeMotion().onFailure { error, _ -> showError(error.description) }
removeMotion() returns a DatResult, so handle its failure branch rather than assuming teardown succeeded.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().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.MotionConfiguration rather than the intervals between recorded timestamps.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.source, whatever the recording contains, so you cannot exercise Meta Neural Band or unknown-source handling through a feed.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
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 }
samples is an AsyncStream, so iterate it in a task that you retain and cancel during teardown. Observe state and errors through their publishers.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.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.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.samplesTask?.cancel() stateToken = nil errorToken = nil motion.stop() try session.removeMotion()
mockMotionGravityMetersPerSecondSquared for the at-rest gravity reading when you synthesize samples.mockDevice.services.motion.setMotionFeed(recordedSamples)
setMotionFeed while a Motion session is streaming, so a feed set mid-session has no effect.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.source.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.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.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.