A seismograph measures how the ground shakes. Real ones are so sensitive that they feel earthquakes on the other side of the planet. Yours sits on your desk and feels much smaller quakes: a pen dropping, someone walking past, a door slamming downstairs. The ESP32 reads the MPU-6050 a hundred times a second, and a web page draws a live trace that jumps with every shake. The biggest shakes go into a list with the time, so you can find out what happened while you were away.
What you'll learn
- How an accelerometer measures gravity and shaking
- How to talk to a sensor over I2C without a library
- How to separate a quick shake from the slow pull of gravity
- How a web page can draw live data by asking every fraction of a second
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- GY-521 MPU-6050 module (45-in-1 Sensor Kit)
- 4 female-to-male jumper wires
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (seismograph base), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| MPU-6050 VCC | 3V3 |
| MPU-6050 GND | GND |
| MPU-6050 SCL | ESP32 GPIO 22 |
| MPU-6050 SDA | ESP32 GPIO 21 |
| XDA, XCL, AD0, INT | Not used |
GPIO 21 and 22 are the ESP32's usual I2C pins. I2C uses only two wires for data, however many sensors you add.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Change TIME_ZONE to your own time zone. The example is for Central Europe; search for "POSIX time zone" and your city to find yours (for example EET-2EEST,M3.5.0/3,M10.5.0/4 for Estonia or GMT0BST,M3.5.0/1,M10.5.0 for the UK).
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <time.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "CET-1CEST,M3.5.0,M10.5.0/3"; // your time zone
const int MPU = 0x68; // I2C address of the MPU-6050
const float EVENT = 0.03; // shake (in g) that goes into the list
const int LIST = 8; // how many big shakes to remember
WebServer server(80);
float baseline = 1.0; // slow average: gravity
float peak = 0; // biggest shake since the page last asked
float eventSize[LIST];
String eventTime[LIST];
int events = 0;
unsigned long lastRead = 0, lastEvent = 0;
// Total acceleration in g (1 g = gravity at rest)
float readG() {
Wire.beginTransmission(MPU);
Wire.write(0x3B); // first acceleration register
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6);
int16_t ax = Wire.read() << 8 | Wire.read();
int16_t ay = Wire.read() << 8 | Wire.read();
int16_t az = Wire.read() << 8 | Wire.read();
float x = ax / 16384.0, y = ay / 16384.0, z = az / 16384.0;
return sqrt(x * x + y * y + z * z);
}
void logEvent(float size) {
for (int i = LIST - 1; i > 0; i--) { // move the list down, newest on top
eventSize[i] = eventSize[i - 1];
eventTime[i] = eventTime[i - 1];
}
time_t now = time(nullptr);
struct tm t;
localtime_r(&now, &t);
char hms[9];
strftime(hms, sizeof(hms), "%H:%M:%S", &t);
eventSize[0] = size;
eventTime[0] = hms;
if (events < LIST) events++;
Serial.printf("Shake %.3f g at %s\n", size, hms);
}
void handleShake() {
String json = "{\"peak\":" + String(peak, 4) + ",\"events\":[";
for (int i = 0; i < events; i++) {
if (i > 0) json += ",";
json += "[\"" + eventTime[i] + "\"," + String(eventSize[i], 3) + "]";
}
json += "]}";
peak = 0; // start a new peak for the next question
server.send(200, "application/json", json);
}
const char PAGE[] = R"html(<!doctype html><meta name=viewport content='width=device-width'><title>Seismograph</title>
<style>body{font-family:sans-serif;max-width:640px;margin:24px auto;padding:0 16px}canvas{width:100%;height:200px;background:#FFFDF6;border:1px solid #ddd;border-radius:12px}td{padding:2px 12px 2px 0}</style>
<h1>Seismograph</h1><canvas id=c width=600 height=200></canvas><p id=now></p><h2>Biggest shakes</h2><table id=list></table>
<script>
const c = document.getElementById('c'), g = c.getContext('2d');
const trace = new Array(300).fill(0);
function draw() {
g.clearRect(0, 0, 600, 200);
g.strokeStyle = '#ddd'; g.beginPath(); g.moveTo(0, 100); g.lineTo(600, 100); g.stroke();
g.strokeStyle = '#1B3A7B'; g.beginPath();
trace.forEach((v, i) => {
const h = Math.min(95, Math.sqrt(v) * 300); // square root shows small shakes too
g.moveTo(i * 2, 100 - h); g.lineTo(i * 2, 100 + h);
});
g.stroke();
}
async function tick() {
try {
const d = await (await fetch('/shake')).json();
trace.push(d.peak); trace.shift(); draw();
document.getElementById('now').textContent = 'Shake now: ' + d.peak.toFixed(4) + ' g';
document.getElementById('list').innerHTML = d.events.map(e => '<tr><td>' + e[0] + '</td><td>' + e[1].toFixed(3) + ' g</td></tr>').join('');
} catch (e) {}
setTimeout(tick, 200);
}
tick();
</script>)html";
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
Wire.beginTransmission(MPU);
Wire.write(0x6B); // power register
Wire.write(0); // wake up
Wire.endTransmission();
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print(" connected. Open http://");
Serial.println(WiFi.localIP());
configTzTime(TIME_ZONE, "pool.ntp.org");
server.on("/", []() { server.send(200, "text/html", PAGE); });
server.on("/shake", handleShake);
server.begin();
}
void loop() {
server.handleClient();
if (millis() - lastRead < 10) return; // 100 readings a second
lastRead = millis();
float g = readG();
baseline = baseline * 0.99 + g * 0.01; // follows gravity slowly
float shake = fabs(g - baseline); // what is left is the shaking
if (shake > peak) peak = shake;
if (shake > EVENT && millis() - lastEvent > 2000) { // one entry per shake, not 50
lastEvent = millis();
logEvent(shake);
}
}
Try it
Open the Serial Monitor at 115200 baud: after connecting, the ESP32 prints its address, such as http://192.168.1.42. Open it in a browser on a phone or computer in the same Wi-Fi.
- Keep the page open and tap the table gently with one finger: the trace jumps.
- Drop a pencil from 10 cm, then from 30 cm. Which makes the bigger shake?
- Leave it running while you are out, and check the list of biggest shakes when you come back. Can you work out what caused them?
How it works
The MPU-6050 contains an accelerometer: tiny springy structures inside the chip bend when it is pushed, and the chip measures how much. At rest it still feels gravity, about 1 g straight down. readG() asks for the three directions over I2C, a two-wire bus: write the number of the first register, then read six bytes, two for each direction. Dividing by 16,384 turns the raw numbers into g.
To find the shaking, the sketch needs to remove gravity. baseline is a slow average that takes only 1 % of each new reading, so it follows gravity but ignores fast changes. The difference between a reading and the baseline is the shake, a high-pass filter in one line of code.
The page asks /shake five times a second. Between two questions the ESP32 takes 20 readings and keeps only the biggest, the peak, so no short tap is lost. The browser adds each peak to a list of 300 and redraws the trace, the newest on the right.
Try this next
- Sound a buzzer when a shake is bigger than 0.2 g, like an earthquake alarm.
- Find out how far away you can still feel someone jumping.
- Count the shakes per hour and find the busiest time of the day in your house.
Troubleshooting
- The trace is always flat and "Shake now" stays at 0: check SDA on GPIO 21 and SCL on GPIO 22, and VCC on 3V3.
- The trace is never quiet: the module is loose in its pocket, or the table wobbles. Fix the module with a drop of glue and try another surface.
-
The list fills up with tiny shakes: raise
EVENT, for example to 0.06. - It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.