Pocket Tilt-O-Meter 3000: Tiny ESP32C3 Tilt and Range Gadget

by jfunky1111 in Circuits > Tools

10 Views, 0 Favorites, 0 Comments

Pocket Tilt-O-Meter 3000: Tiny ESP32C3 Tilt and Range Gadget

20260307_184031.jpg
Screenshot 2026-03-06 184015.png

This project is a compact tool that combines a digital level and a ruler in one device.


At its heart is a Seeed Studio XIAO ESP32-C3 microcontroller. It reads distance from an HC-SR04T ultrasonic sensor and tilt from an MPU6050 accelerometer/gyroscope. The measurements are shown in real time on a 64×32 SSD1306 I2C OLED display and on a WiFi page.


You can use it to:


  1. Check if shelves, frames, or furniture are level
  2. Measure distances like a digital ruler without a tape
  3. Align projects in the workshop with both angle and distance feedback

Because the XIAO ESP32-C3 is small and low power, the whole device can be made very compact and portable. In this Instructable, I will show how to wire the parts, program the microcontroller, and use the screen to display distance and tilt clearly so you can use it as a handy level-and-ruler tool in your shop or on the go.

Supplies

61PPeH-ZPUL._AC_UY218_.jpg
51r16UPUYEL._SL1050_.jpg
51rkuTJOm3L._AC_UY218_.jpg
71j5mOwjPuL._SL1500_.jpg
  1. Seeed Studio XIAO ESP32-C3 microcontroller
  2. HC-SR04T ultrasonic distance sensor
  3. MPU6050 accelerometer and gyroscope module
  4. 64×32 SSD1306 I2C OLED display
  5. Soldering iron
  6. Hot glue gun
  7. USB-C cable for programming the XIAO ESP32-C3
  8. Small 3.7v LiPo battery
  9. Arduino IDE


Assembly

Untitled drawing (2).png
20260306_181505.jpg
20260307_180649.jpg


-----WIRING-----

Power

  1. VCC of all boards to ESP32 3v3 pin
  2. GND of all boards to the ESP32 GND pin
  3. LiPo + to BAT+ on ESP32 back
  4. LiPo - to BAT- on ESP32 back

Signal

  1. All SDA to D4
  2. All SCL to D5

-----3D PRINTING-----

Download Tilt-O-Meter 3000.stl

(This was created with Autodesk TinkerCad, my favorite 3D modeling software, even though it is simple.)

Print it (I used Ender 3 V2)

----HOT GLUE!!!----

In the shorter end of the 3D printed parts, insert the HC-SR04T with the white plastic connector sticking out the hole on the end and glue it in.

Insert the power switch in the other hole on the other part.

On the short side of the parts on the outside, glue the screen.

On the longer part, glue the ESP32 in the small divot in the edge so the port sticks out.

Glue the MPU6050 to the inside of the opposite side on which you placed the LCD screen.

Plug the HC-SR04 cable into the white connector.

Coding

Screenshot 2026-03-09 085834.png
Screenshot 2026-03-09 085823.png
Screenshot 2026-03-09 091546.png

Get Arduino IDE and open a new sketch.

Paste this:

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <MPU6050.h>

const char* ssid = "Tilt-O-Meter 3000";
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

#define SCREEN_WIDTH 64
#define SCREEN_HEIGHT 32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
MPU6050 mpu;

float readings[3] = {0, 0, 0};
int readIndex = 0;
float currentAvgInches = 0;
unsigned long lastUpdate = 0;
float pitch = 0, roll = 0;

// --- SPIRIT LEVEL WEBPAGE (NO INTERNET NEEDED) ---
String getHTML() {
String ptr = "<!DOCTYPE html><html><head><title>Tilt-O-Meter 3000</title>";
ptr += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
ptr += "<style>body{margin:0;background:#080808;color:#fff;font-family:sans-serif;text-align:center;}";
ptr += ".val{color:#00ffcc;font-size:3.5em;font-family:monospace;text-shadow:0 0 10px #00ffcc;}";
ptr += "#level{width:180px;height:180px;border:3px solid #444;border-radius:50%;margin:30px auto;position:relative;background:radial-gradient(#111,#000);}";
ptr += "#bubble{width:35px;height:35px;background:#00ffcc;border-radius:50%;position:absolute;top:72px;left:72px;box-shadow:0 0 20px #00ffcc;transition:0.05s;}";
ptr += ".target{width:40px;height:40px;border:1px solid #00ffcc;border-radius:50%;position:absolute;top:70px;left:70px;opacity:0.3;}</style></head>";
ptr += "<body><div style='margin-top:20px;letter-spacing:2px;font-size:0.8em;color:#888'>DISTANCE</div><div id='dist' class='val'>0.0</div><div style='font-size:0.7em;color:#888'>INCHES</div>";
ptr += "<div id='level'><div class='target'></div><div id='bubble'></div></div>";
ptr += "<div style='font-size:0.9em;color:#555'>XIAO ESP32-S3 LIVE</div>";
ptr += "<script>var websocket=new WebSocket('ws://'+window.location.hostname+'/ws');";
ptr += "websocket.onmessage=function(event){var data=JSON.parse(event.data);";
ptr += "document.getElementById('dist').innerHTML=data.d.toFixed(1);";
ptr += "var b=document.getElementById('bubble');";
ptr += "var x=72+(data.r*2.5); var y=72+(data.p*2.5);";
ptr += "b.style.left=Math.max(0,Math.min(145,x))+'px';";
ptr += "b.style.top=Math.max(0,Math.min(145,y))+'px';";
ptr += "if(Math.abs(data.p)<0.5 && Math.abs(data.r)<0.5) b.style.background='#00ff00'; else b.style.background='#00ffcc';};</script></body></html>";
return ptr;
}

void setup() {
Wire.begin(D4, D5, 100000);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
mpu.initialize();

WiFi.softAP(ssid);
server.addHandler(&ws);
server.on("/", HTTP_GET, [](AsyncWebServerRequest *r){ r->send(200, "text/html", getHTML()); });
server.begin();
}

void loop() {
unsigned long now = millis();

ws.cleanupClients();

if (now - lastUpdate >= 100) {
lastUpdate = now;


Wire.beginTransmission(0x57);
Wire.write(0x01);
Wire.endTransmission();

delayMicroseconds(500);


Wire.requestFrom(0x57, 3);
if (Wire.available() >= 3) {
uint32_t val = ((uint32_t)Wire.read() << 16) | ((uint32_t)Wire.read() << 8) | Wire.read();
float newInch = (val / 1000.0) / 25.4;
if(newInch > 0.1) {
readings[readIndex] = newInch;
readIndex = (readIndex + 1) % 3;
currentAvgInches = (readings[0] + readings[1] + readings[2]) / 3.0;
}
}


int16_t ax, ay, az;
mpu.getAcceleration(&ax, &ay, &az);
pitch = atan2((float)ay, (float)az) * 180 / M_PI;
roll = atan2(-(float)ax, sqrt((float)ay * ay + (float)az * az)) * 180 / M_PI;

String json = "{\"d\":" + String(currentAvgInches, 1) + ",\"p\":" + String(pitch, 1) + ",\"r\":" + String(roll, 1) + "}";
ws.textAll(json);


display.clearDisplay();
display.setCursor(0,0);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.printf("In: %.1f\n", currentAvgInches);
display.println(abs(pitch) < 0.8 ? "LEVEL" : (pitch > 0 ? "TILT FWD" : "TILT BACK"));
display.printf("%s", abs(roll) < 0.8 ? "LEVEL" : (roll > 0 ? "TILT LEFT" : "TILT RIGHT"));
display.display();
}
}


For both of these libraries, click on Library Manager, search for it, and install. Supporting images are in the step.

  1. MPU6050 by Electronic Cats
  2. Adafruit SSD1306 by Adafruit

Go to Preferences -> Additional Board URL's and paste this: https://espressif.github.io/arduino-esp32/package_esp32_index.json

Go to Board Manager, search ESP32, and click install.

Plug in your ESP32.

Select XIAO ESP32-C3

Select Upload!

Your screen should show the distance and how you need to tilt it.

WiFi Interface

Screenshot 2026-03-06 183938.png

Connect the antenna to the connector between the boot and reset buttons.

Look for a network called Tilt-O-Meter 3000. It should be an open network.

Connect to it, and go to 192.168.4.1 in a web browser.

The web interface should appear with the distance and a moving representation of the tilt!

Customization and Usage

20260307_184059.jpg
20260306_181514.jpg
20260307_184039.jpg

Using It as a Level

  1. Place the device on a shelf, frame, or piece of wood.
  2. Watch the OLED:
  3. When both pitch and roll say LEVEL, the surface is flat enough for most projects.
  4. If it shows TILT FWD/BACK/LEFT/RIGHT, adjust the object until the text changes to LEVEL.
  5. You can also use just the web interface and watch the “bubble” move into the center circle.

Using It as a Digital Ruler

  1. Point the ultrasonic sensor at the target you want to measure (wall, cabinet, etc.).
  2. Hold the enclosure so the sensor is roughly perpendicular to the target.
  3. Read the distance in inches on either:
  4. The OLED display, or
  5. The WiFi page (the big distance readout).
  6. This is handy for:
  7. Quick room measurements
  8. Checking clearances on the workbench
  9. Measuring how far a drawer extends

Ideas for Further Customization

  1. Add a small buzzer that beeps when the device is perfectly level.
  2. Change the WiFi SSID in the code so you can tell multiple tools apart.
  3. Update the CSS colors in getHTML() to match your favorite theme (dark, neon, workshop colors, etc.).
  4. 3D print a different case that fits on a tripod, camera shoe, or the side of a tool.


Get out there and get measuring!!!

-James F