Make a Wi-Fi spirit level

Turn the MPU-6050 and the ESP32 into a spirit level with a moving bubble on your phone's screen.
  • Level:Intermediate
  • Duration: 50 min
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Make a Wi-Fi spirit level – EEMENTOR STEM™ lesson

Builders use a spirit level to check that shelves are straight. In this lesson you build a digital one: the MPU-6050 measures the tilt, and your phone shows a bubble that moves as you tilt the breadboard, just like the bubble in a real level.

What you'll learn

  • How to use the MPU-6050 with the ESP32 over I2C
  • How to calculate tilt angles
  • How to animate a web page with live data

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • GY-521 MPU-6050 module (45-in-1 Sensor Kit)
  • A 2.4 GHz Wi-Fi network
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Make a Wi-Fi spirit level
Breadboard view. Check each connection against the wiring table below.
Module pin ESP32 pin
VCC 3V3
GND GND
SCL GPIO 22
SDA GPIO 21
XDA, XCL, AD0, INT Not connected

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

The sketch

Install the Adafruit MPU6050 library and its dependencies, as in the MPU-6050 lesson.

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only). Open the Serial Monitor at 115200 baud to see the address, then open it on a phone or computer on the same Wi-Fi.

#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

Adafruit_MPU6050 mpu;
WebServer server(80);

const char PAGE[] = R"rawliteral(
<!DOCTYPE html><html><head><meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'><title>Spirit level</title></head>
<body style='font-family:sans-serif;text-align:center;padding:16px'>
<h1>Spirit level</h1>
<div style='position:relative;width:260px;height:260px;margin:auto;border-radius:50%;background:#e8f5d0;border:4px solid #1B3A7B'>
  <div style='position:absolute;left:115px;top:115px;width:30px;height:30px;border-radius:50%;border:2px dashed #888'></div>
  <div id='bubble' style='position:absolute;width:40px;height:40px;border-radius:50%;background:#7cc242;left:110px;top:110px;transition:all .15s'></div>
</div>
<p id='angles'>-</p>
<script>
async function update() {
  const d = await (await fetch('/angles')).json();
  const clamp = (v) => Math.max(-30, Math.min(30, v));
  const b = document.getElementById('bubble');
  b.style.left = (110 - clamp(d.roll) * 3.5) + 'px';
  b.style.top = (110 + clamp(d.pitch) * 3.5) + 'px';
  document.getElementById('angles').textContent = 'Pitch ' + d.pitch.toFixed(1) + '°  Roll ' + d.roll.toFixed(1) + '°';
}
setInterval(update, 200);
</script></body></html>
)rawliteral";

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);  // SDA, SCL
  if (!mpu.begin()) {
    Serial.println("MPU-6050 not found, check the wiring");
    while (true) delay(10);
  }
  mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.print("Open http://");
  Serial.println(WiFi.localIP());

  server.on("/", []() { server.send(200, "text/html", PAGE); });
  server.on("/angles", []() {
    sensors_event_t a, g, t;
    mpu.getEvent(&a, &g, &t);
    float ax = a.acceleration.x, ay = a.acceleration.y, az = a.acceleration.z;
    float pitch = atan2(ax, sqrt(ay * ay + az * az)) * 180.0 / PI;
    float roll = atan2(ay, az) * 180.0 / PI;
    server.send(200, "application/json", "{\"pitch\":" + String(pitch, 1) + ",\"roll\":" + String(roll, 1) + "}");
  });
  server.begin();
}

void loop() {
  server.handleClient();
}

How it works

When the sensor lies still, the only force it feels is gravity. Tilting it spreads gravity across its X, Y and Z axes, and atan2() turns that into pitch and roll angles. Five times a second the web page asks for the angles and moves the bubble the opposite way to the tilt, like an air bubble that floats to the highest point. When both angles are near 0°, the bubble sits in the dashed circle: level.

Try this next

  • Turn the circle green when the board is within 1° of level.
  • Add a "zero" button that sets the current position as level.
  • Show the angles on the 1602 LCD as well, for a level that works without a phone.

Troubleshooting

  • "MPU-6050 not found": check that SDA is on GPIO 21 and SCL on GPIO 22.
  • The bubble moves the wrong way: swap the + and − signs in the bubble position lines.
  • It isn't quite 0° on a flat table: small offsets are normal; add a zero button (see above).

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit