> ## Documentation Index
> Fetch the complete documentation index at: https://docs.edgeimpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Motion inference on Wear OS

> Classify motion on a Wear OS smartwatch with an Edge Impulse model that reads the watch's accelerometer and gyroscope.

This tutorial runs an Edge Impulse motion classification model on a Wear OS smartwatch. The app reads the accelerometer through `SensorManager`, fills a buffer sized to the model's window, and runs the C++ classifier through JNI each time the buffer is full. Jetpack Compose for Wear OS shows the result on the watch face.

The [keyword spotting](https://docs.edgeimpulse.com/tutorials/topics/android/keyword-spotting.md) example uses the same buffered approach for audio on a phone.

![Round Wear OS watch face listing motion classification labels and confidence scores](https://mintcdn.com/edgeimpulse/XwnzH-Jo3nDoEg0b/.assets/images/android/wearos-sensors.png?fit=max\&auto=format\&n=XwnzH-Jo3nDoEg0b\&q=85\&s=24e14597ea346ddfcfdf09639d638642)

Wear OS app showing motion classification results on a round watch face

## Prerequisites

* A motion classification model trained in Edge Impulse on accelerometer data, and optionally gyroscope data
* Android Studio with NDK 27.0.12077973 and CMake 3.22.1 installed
* A Wear OS device or emulator running API 30 or later

## 1. Clone the repository

```bash theme={"system"}
git clone https://github.com/edgeimpulse/example-android-inferencing.git
cd example-android-inferencing/example_motion_WearOS
```

## 2. Download TensorFlow Lite libraries

```bash theme={"system"}
cd app/src/main/cpp/tensorflow-lite

# macOS/Linux
sh download_tflite_libs.sh

# Windows
download_tflite_libs.bat
```

## 3. Export your model

1. In Edge Impulse Studio, go to **Deployment**.
2. Select **Android (C++ library)**.
3. Enable **EON Compiler** to reduce inference time and battery use.
4. Click **Build** and download the `.zip` file.

## 4. Integrate the model

Extract the downloaded `.zip` file and copy its contents, except `CMakeLists.txt`, into `app/src/main/cpp/`. The folder then looks like this:

```text theme={"system"}
app/src/main/cpp/
├── edge-impulse-sdk/
├── model-parameters/
├── tflite-model/
├── tensorflow-lite/
├── wearosinference.cpp
└── CMakeLists.txt   # keep the existing file
```

## 5. Match the input frame size

The model expects a fixed number of float values as input. Find the value in `model_metadata.h`:

```cpp theme={"system"}
#define EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE 375
```

Then update `MainActivity.kt` to match:

```kotlin theme={"system"}
// If your model uses 375 floats (125 samples × 3 axes), set this to 375
private const val EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE = 375
```

If the values don't match, the classifier returns incorrect results or crashes.

## 6. Build and run

1. Open the project in Android Studio.
2. Select **Build** -> **Make Project**.
3. Connect your Wear OS device or start a Wear OS emulator.
4. Run the app.

Perform one of your trained movements. The classification result appears on the watch face.

## How it works

### Sensor setup

The app registers the accelerometer listener in `onResume` and removes it in `onPause`, so the sensor doesn't drain the battery while the app isn't visible:

```kotlin theme={"system"}
override fun onResume() {
    super.onResume()
    accelerometer?.also {
        sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_NORMAL)
    }
}

override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}
```

### Ring buffer collection

Each sensor event writes its x, y, and z values into the buffer. When the buffer is full, the app runs inference and resets the index:

```kotlin theme={"system"}
private val ringBuffer = FloatArray(EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE)
private var ringBufferIndex = 0

override fun onSensorChanged(event: SensorEvent) {
    when (event.sensor.type) {
        Sensor.TYPE_ACCELEROMETER -> {
            ringBuffer[ringBufferIndex++] = event.values[0] // x
            ringBuffer[ringBufferIndex++] = event.values[1] // y
            ringBuffer[ringBufferIndex++] = event.values[2] // z

            if (ringBufferIndex >= EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
                ringBufferIndex = 0
                runInference(ringBuffer)
            }
        }
    }
}
```

### Native inference

The float array crosses the JNI boundary, and the native code passes it to the classifier:

```cpp theme={"system"}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_edgeimpulse_edgeimpulsewearos_presentation_MainActivity_runInference(
        JNIEnv* env, jobject,
        jfloatArray data) {

    jsize len = env->GetArrayLength(data);
    jfloat* elements = env->GetFloatArrayElements(data, nullptr);
    std::vector<float> features(elements, elements + len);

    signal_t signal;
    signal.total_length = features.size();
    signal.get_data = &get_signal_data;

    ei_impulse_result_t result = {0};
    run_classifier(&signal, &result, false);

    return formatResult(env, result);
}
```

### Wear OS UI

The app shows the result in a Jetpack Compose for Wear OS `Chip`:

```kotlin theme={"system"}
@Composable
fun InferenceChip(inferenceText: String) {
    Chip(
        onClick = { },
        label = {
            Text(
                text = inferenceText,
                style = MaterialTheme.typography.body1
            )
        },
        modifier = Modifier.fillMaxWidth()
    )
}
```

## Adding gyroscope data

If your model was trained with both accelerometer and gyroscope, register both sensors and write all six values per sample:

```kotlin theme={"system"}
override fun onSensorChanged(event: SensorEvent) {
    when (event.sensor.type) {
        Sensor.TYPE_ACCELEROMETER -> {
            ringBuffer[ringBufferIndex++] = event.values[0]
            ringBuffer[ringBufferIndex++] = event.values[1]
            ringBuffer[ringBufferIndex++] = event.values[2]
        }
        Sensor.TYPE_GYROSCOPE -> {
            ringBuffer[ringBufferIndex++] = event.values[0]
            ringBuffer[ringBufferIndex++] = event.values[1]
            ringBuffer[ringBufferIndex++] = event.values[2]
        }
    }

    if (ringBufferIndex >= EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
        ringBufferIndex = 0
        runInference(ringBuffer.clone())
    }
}
```

The axis order in the buffer must match the order you used when you collected the training data.

## Troubleshooting

> **Incorrect or unstable classifications**
>
> * Check that `EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE` matches your model's input size.
> * Check that the sensor sampling rate matches your training data. If you trained at a higher rate, change `SENSOR_DELAY_NORMAL` to `SENSOR_DELAY_GAME` (about 50 Hz).
> * Check that the axis order in the buffer matches your training data.

> **High battery drain**
>
> * Keep the `unregisterListener` call in `onPause()`.
> * Lower the sampling rate to the minimum your model needs.
> * Enable **EON Compiler** when you build the deployment.

> **No sensor data on emulator**
>
> * Use the emulator's extended controls to inject sensor data.
> * Test on a physical Wear OS device for realistic sensor data.

## Next steps

* [Keyword spotting on Android](https://docs.edgeimpulse.com/tutorials/topics/android/keyword-spotting.md)
* [Android series overview](https://docs.edgeimpulse.com/tutorials/topics/android/android-series.md)

## Resources

* [GitHub: example\_motion\_WearOS](https://github.com/edgeimpulse/example-android-inferencing/tree/main/example_motion_WearOS)
* [Continuous motion recognition tutorial](https://docs.edgeimpulse.com/tutorials/end-to-end/motion-recognition.md)
* [Wear OS development guide](https://developer.android.com/training/wearables)
