<p>I clapped twice. The lights went off.</p><p>No voice command. No phone. No hub. Just a Galaxy Watch 4 on my wrist, an ML model running on its processor, and a UDP (User Datagram Protocol) packet fired across the local network.</p><p>I built it all myself, using Edge Impulse. Here is how it works.</p>
<div class="kg-video-container">
<div class="kg-video-overlay">
</svg>
</button>
</div>
<div class="kg-video-player-container">
<div class="kg-video-player">
</svg>
</button>
</svg>
</button>
0:00
<div class="kg-video-time">
/1:58
</div>
1×
</svg>
</button>
</svg>
</button>
</div>
</div>
</div>
<figcaption><p>Gesture-controlled smart lights demo using Edge Impulse and a Galaxy watch </p></figcaption>
</figure><h2 id="the-idea">The idea</h2><p>Smartwatches are underrated as gesture remotes. They sit on your wrist, they have an IMU, and — crucially — they are already there when you want to adjust the lights without hunting for your phone. The catch is that getting a custom ML model onto a Wear OS watch is supposed to be painful: ABI mismatches, stripped-down runtimes, JNI boilerplate, toolchains that argue with each other.</p><p>Two things in particular are worth calling out:</p><p>Capturing training data from a wearable is normally a project in itself. The watch does not expose a USB data port you can plug into a laptop. You have to build a companion app, handle Bluetooth or Wi-Fi transport, and somehow get the samples into whatever format your training pipeline expects. Here, I built a small forwarding app that streams IMU data directly to the Edge Impulse ingestion API — so the watch becomes a first-class data source without any intermediate tooling.</p><p>Deploying to a wearable is where most TFLite-based projects stall. Pre-built Android TFLite libraries ship arm64-v8a binaries, but the Galaxy Watch 4 runs a 32-bit armeabi-v7a userspace despite its 64-bit processor. Edge Impulse’s C++ Android deployment sidesteps the problem entirely by exporting TFLite Micro source code alongside the model. CMake compiles everything from scratch targeting whatever ABI you specify. One line in build.gradle.kts:</p><pre>ndk { abiFilters.add("armeabi-v7a") }
</pre><p>That is the entire ABI configuration. Everything else is handled by the build.</p><h2 id="collecting-data">Collecting data</h2><p>The Galaxy Watch 4 exposes accelerometer and gyroscope data via the standard Android SensorManager API at up to 50 Hz. I built a small data-forwarding app that streamed both sensors over UDP to the Edge Impulse data ingestion endpoint — six channels in total: acc_x, acc_y, acc_z, gyr_x, gyr_y, gyr_z.</p><p>I recorded three gesture classes:</p><ul><li>double_clap — two sharp claps</li><li>color_cycle — two quick wrist twists</li><li>idle — wrist at rest, normal movement</li></ul><p>About 30 samples per class, two seconds per sample, with each sample containing a single gesture performed once within that window. The whole collection session took roughly 20 minutes.</p><p>I created a dedicated application to collect sensor data from the watch and upload it directly to my Edge Impulse project using the project API key. Here is how it works:</p>
<div class="kg-video-container">
<div class="kg-video-overlay">
</svg>
</button>
</div>
<div class="kg-video-player-container">
<div class="kg-video-player">
</svg>
</button>
</svg>
</button>
0:00
<div class="kg-video-time">
/1:22
</div>
1×
</svg>
</button>
</svg>
</button>
</div>
</div>
</div>
<figcaption><p>Data collection demo using Edge Impulse and a Galaxy watch </p></figcaption>
</figure><p>You can download the app source from the smartwatch_data_collector repository on GitHub and follow the instructions in the README to add your own project’s API key using the adb command.</p><p>After collection, samples were reviewed and cleaned using Edge Impulse Studio’s sample editor — this involved trimming samples where the gesture started too late or was cut off at the edge of the window, and removing any samples with obvious sensor dropout.</p> <figcaption>Gesture data sample example </figcaption> <hr /><h2 id="configuring-the-impulse">Configuring the impulse</h2><p>The impulse uses IMU sensor data — accelerometer and gyroscope — as input, with a 2-second window at 50 Hz giving 100 data points per channel. A spectral analysis processing block extracts frequency-domain features from each axis, which are then fed into a small dense neural network classifier.</p> <figcaption>Impulse configuration in Studio </figcaption> <h2 id="extracting-features">Extracting features</h2><p>With some fine-tuning of the spectral analysis block parameters, the features for double_clap, color_cycle, and idle became clearly separable in the feature explorer.</p> <figcaption>Generated features overview in Studio </figcaption> <h2 id="training">Training</h2><p>Training is straightforward inside Edge Impulse Studio. After training, I selected the int8 post-training quantized variant to keep the model compact and suitable for on-device inference. Final accuracy on the test set came out at 92% , with most of the misses being due to the limited number of samples in the training dataset.</p> <figcaption>Training validation accuracy and confusion matrix in Studio </figcaption> <h2 id="wiring-it-up">Wiring it up</h2><p>The inference pipeline in the watch app runs in two stages.</p><p>First, a motion detector monitors the accelerometer variance over a rolling 10-sample window at 50 ms intervals. At rest, variance sits near zero. A gesture spikes it above a threshold almost immediately — no model needed for this step, just arithmetic. This keeps the CPU nearly idle when nothing is happening.</p><p>When motion is detected, the app waits one second to let the gesture fully develop, then passes the last two seconds of IMU data to the Edge Impulse classifier via a JNI call:</p><pre>val result = runInference(sensorCollector.buildInputArray(featureCount))
val label = result?.substringBefore(":")
val confidence = result?.substringAfter(":")?.toFloatOrNull() ?: 0f
</pre><p>Results below 65% confidence are discarded. Everything else maps to a smart light command — toggle on/off or color cycle — sent as a UDP packet to the bulb’s local IP.</p><p>A partial wake lock keeps the sensor pipeline alive when the screen is off, which is the normal state for a watch.</p><h2 id="results">Results</h2><p>End-to-end latency from gesture completion to bulb response is around 1.1 seconds — the one-second capture window dominates. That is perfectly comfortable for light control; you are not trying to play a video game.</p><p>The model runs reliably across normal daily movement. The variance gate filters out walking and typing almost completely, so false triggers are rare in practice.</p><h2 id="what%E2%80%99s-next">What’s next</h2><p>The obvious improvement is more robust training data — more samples, more variation in wrist angle and movement speed. Beyond that, the same architecture would support scene presets (a slow double-tap for “movie mode”, a Z-motion for “goodnight”), which would make this genuinely useful rather than just a satisfying demo. Furthermore, more devices can be added to demonstrate extended functionalities within a smart home setting.</p><p>Want to go deeper? The full source code, including the Edge Impulse deployment, the Wear OS app, and a helper script to swap in a retrained model in one command, is on GitHub in the smartwatch_gesture_control repository .</p>