IMU Air Mouse
A BLE HID mouse that uses the onboard IMU of a Seeed XIAO nRF52840 Sense to move the cursor by tilting and rotating the device in the air. Intended as an open-source replacement for presentation remotes like the Logitech Spotlight, with additional features.
Features
- 6-DoF gyro + accelerometer via LSM6DS3 with complementary filter
- Hardware tap detection — single tap = left click, double tap = right click
- BLE HID mouse — works natively on Windows, macOS, Linux, Android, iOS
- BLE Battery Service — charge level visible in OS Bluetooth settings
- Web config UI (
web/index.html) — configure over BLE from any Chrome/Edge browser, no app install - Flash persistence — config survives power cycles (LittleFS)
- Live IMU stream — 20 Hz gyro/accel data streamed to the web UI visualiser
- Live telemetry — temperature, uptime, click counts, gyro bias RMS, recal count
- Temperature compensation — gyro drift correction by Δ temperature since last calibration
- Auto-recalibration — recalibrates automatically after 5 minutes of idle
- Configurable charge rate — OFF / 50 mA slow / 100 mA fast via BQ25100 HICHG pin
- Boot-loop detection — 3 rapid reboots trigger safe mode (config service disabled, flash wiped)
Hardware
| Part | Notes |
|---|---|
| Seeed XIAO nRF52840 Sense | nRF52840 + LSM6DS3 IMU onboard |
| Li-ion battery (10440 / LiPo) | Connected to VBAT + GND pads |
LED Status
The XIAO has three user LEDs (active LOW — HIGH = off, LOW = on):
| LED | Pattern | Meaning |
|---|---|---|
| Blue | Single pulse every 10 s | BLE connected (heartbeat) |
| Green | Single pulse every 10 s | Advertising / not connected (heartbeat) |
| Green | Rapid flutter (~10 Hz) | Gyro calibration in progress |
| Red | Fast blink (continuous) | IMU init failed — hardware fault |
| Red | 3 slow blinks on boot | Boot-loop detected — entered safe mode |
| Red | 6 rapid blinks | Battery critically low (< 3.10 V) |
Blue = BLE-related state. Green = device activity. Red = fault only.
Web Config UI
Open web/index.html in Chrome or Edge (desktop). Requires Web Bluetooth — enable it at chrome://flags/#enable-web-bluetooth on Linux.
Configurable parameters:
| Parameter | Range | Description |
|---|---|---|
| Sensitivity | 100 – 1500 | Cursor speed multiplier |
| Dead zone | 0.005 – 0.2 rad/s | Noise floor; raise to reduce drift |
| Accel strength | 0 – 0.5 | Pointer acceleration multiplier |
| Curve | Linear / Square / √Sqrt | Response shape for input magnitude |
| Flip X / Y | on/off | Invert horizontal or vertical axis |
| Charge mode | Off / 50 mA / 100 mA | BQ25100 charge current |
Commands:
- Calibrate Gyro — recalculates bias offset; hold the device still on a flat surface for ~1 s
- Factory Reset — wipes flash config, restores defaults
Building
Requirements
- PlatformIO (VS Code extension or CLI)
adafruit-nrfutilfor flashing:pip install adafruit-nrfutil
Flash
pio run -t upload
Double-tap the reset button to enter the UF2 bootloader (red LED pulses) if the board doesn't auto-reset.
Feature flags
All optional features are enabled by #define in source/config.h. Comment out any line to disable that feature and reduce firmware size / RAM:
#define FEATURE_CONFIG_SERVICE // BLE config GATT service
#define FEATURE_TELEMETRY // 1 Hz telemetry notifications
#define FEATURE_IMU_STREAM // 20 Hz raw IMU stream
#define FEATURE_TAP_DETECTION // Hardware tap → click engine
#define FEATURE_TEMP_COMPENSATION // Gyro drift correction by temperature
#define FEATURE_AUTO_RECAL // Auto-recalibrate after 5 min idle
#define FEATURE_BATTERY_MONITOR // ADC battery level + BLE Battery Service
#define FEATURE_BOOT_LOOP_DETECT // Crash counter → safe mode
BSP memory layout fix
The XIAO nRF52840 Sense requires a modified linker script to work with SoftDevice S140 7.3.0. Edit cores/linker/nrf52840_s140_v7.ld in the Adafruit nRF52 Arduino core:
MEMORY
{
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
RAM (rwx) : ORIGIN = 0x2000E000, LENGTH = 0x20040000 - 0x2000E000
}
SECTIONS
{
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
} INSERT AFTER .data;
INCLUDE "nrf52_common.ld";