Build a desk seismograph that draws shakes on a web page

An MPU-6050 in a printed seismograph feels every tap, footstep and slammed door. A web page draws the shakes live like a real seismograph and keeps a list of the biggest ones.
  • Level:Intermediate
  • Duration: 1 h
  • Ages 11+
  • Platform: ESP32

Tools for this lesson

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

Build a desk seismograph that draws shakes on a web page – EEMENTOR STEM™ lesson

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

Breadboard wiring diagram: Build a desk seismograph that draws shakes on a web page
Breadboard view. Check each connection against the wiring table below.
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.

  1. Keep the page open and tap the table gently with one finger: the trace jumps.
  2. Drop a pencil from 10 cm, then from 30 cm. Which makes the bigger shake?
  3. 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.

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