Spirit Level

A precision circular bubble level driven by the QMI8658 accelerometer — color-coded tilt zones, low-pass smoothing, and tap-to-calibrate at ~60 fps.

QMI8658 Accel Arduino_Canvas Low-pass filter α=0.85 Tap calibration ±1° green zone ~60 fps

What it does

The QMI8658 is configured in accelerometer-only mode (2 g range, 1000 Hz ODR) for maximum leveling precision. Pitch and roll are derived from the three gravity components using atan2, then fed through a high-coefficient low-pass filter (α = 0.85) to produce a smooth, jitter-free bubble.

The bubble is drawn inside a circular bowl with degree rings at ±5° and ±10°. Its color shifts from green (level) through yellow and orange to red as tilt increases. Tapping the screen zeroes the current tilt as the new reference — useful when mounting on an already-sloped surface.

# Tilt Color Zones

Total tiltColorMeaning
< 1.0°GreenLevel — display also shows * LEVEL * banner
1.0° – 3.0°YellowSlightly off — usable for most applications
3.0° – 7.0°OrangeNoticeable tilt — needs adjustment
> 7.0°RedSignificantly tilted

# Signal Pipeline

StageDetail
IMU read6 bytes from QMI8658 at 0x35. Accel-only mode; 2 g range → 16384 LSB/g. Gyro disabled (CTRL7 = 0x01) to reduce noise floor.
Angle calcpitch = atan2(ay, √(ax²+az²)) × 180/π  |  roll = atan2(−ax, az) × 180/π
Low-pass filtersmooth = 0.85 × prev + 0.15 × raw. High alpha trades lag for stability — ideal for a level where fast response is less important than steadiness.
Calibration offsetOn tap: calPitch = smoothPitch; calRoll = smoothRoll. Displayed angle = smooth − cal.
Bubble clampBubble pixel offset capped at BOWL_R − BUBBLE_R − 4 px so it never exits the bowl regardless of actual tilt angle.

# IMU Configuration

Register setup

CTRL1 (0x02) = 0x40  // auto-increment
CTRL2 (0x03) = 0x03  // 2g, 1000 Hz ODR
CTRL7 (0x08) = 0x01  // accel only

Gyro is left off — it contributes noise without benefit when only static tilt is needed.

Scale

±2 g range → 16384 LSB/g
(vs 8192 LSB/g for ±4 g used
 in other sketches)

2× finer resolution at low tilt
→ bubble reacts to <0.06° steps

# Bowl Geometry

Layout constants

CX = 120, CY = 128   // bowl center
BOWL_R   = 95 px     // outer radius
BUBBLE_R = 14 px     // bubble radius
RANGE    = 15.0°     // deg at bowl edge

Degree rings

// Ring radius for N degrees:
r = N / RANGE × (BOWL_R − BUBBLE_R − 4)

5°  ring → ~27 px from center
10° ring → ~53 px from center

# Rendering

All drawing targets an Arduino_Canvas off-screen buffer in PSRAM. Each frame: fill background, draw bowl rings and crosshair, place bubble with shadow and highlight glint, render pitch/roll readouts, then canvas->flush() in one DMA burst. At 16 ms per loop this achieves ~60 fps with no visible tearing.

# Source

Full sketch in the ESP32-S3-LCD reposketches/spirit_level/spirit_level.ino.

git clone https://github.com/binRick/ESP32-S3-LCD
bash flash-sketch-004-spirit-level.sh