When an older person falls at home, the fall itself is often not the worst part. The worst part is lying on the floor for hours because they can't get up and nobody knows. That is why many families use a fall detector: a small device worn on a belt or around the neck that calls for help by itself. In this lesson you build one with the ESP32 and the MPU-6050 motion sensor. It doesn't just look for a hard bump, because sitting down quickly or dropping onto the sofa can make bumps too. It checks three things, like real fall detectors do: a hard impact, then the body lying on its side or back, then lying still. Then it beeps for 30 seconds so the person can press "I'm OK", and only if nobody presses does it send an urgent message to the family's phones. The same button, held for 3 seconds, calls for help at any time.
What you'll learn
- How to measure the total force of a movement from the three accelerometer axes
- How to tell a fall from sitting down: impact, then lying, then lying still
- How to measure the angle between "standing" and "now" with a dot product
- Why a good alarm asks "Are you OK?" first, and how to avoid false alarms
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- GY-521 MPU-6050 module (45-in-1 Sensor Kit)
- 1 push button (ESP32 IoT Starter Kit)
- Red LED and a 220 Ω resistor (ESP32 IoT Starter Kit)
- Passive buzzer (ESP32 IoT Starter Kit)
- Phones with the free ntfy app for the family
- A small USB power bank, to wear it (not in the kits)
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
A learning project, not a medical alarm. It can miss a fall (for example a slow slide to the floor) and it only works where your Wi-Fi reaches. Real fall alarms are tested, work everywhere with a mobile connection and are often connected to a 24-hour service. Build it with the person who will wear it and with their agreement. In an emergency, always call the emergency number.
Wiring

Build it step by step
Plug in all the parts first, then add the wires in this order. In each picture the new wires are bright and the ones from earlier steps are faded. Click a picture to see it larger.





| From | To |
|---|---|
| MPU-6050 VCC | ESP32 3V3 |
| MPU-6050 GND | GND |
| MPU-6050 SCL | ESP32 GPIO 22 |
| MPU-6050 SDA | ESP32 GPIO 21 |
| Push button, one leg | ESP32 GPIO 32 |
| Push button, the diagonally opposite leg | GND |
| ESP32 GPIO 26 | 220 Ω resistor, then the red LED's long leg |
| Red LED short leg | GND |
| Passive buzzer + | ESP32 GPIO 33 |
| Passive buzzer − | GND |
Fix the sensor firmly to the breadboard or the box with tape: if it wobbles, every step looks like a small impact. The detector works best worn on a belt at the waist, the same way up every time.
Set up ntfy
The phone messages go through ntfy, a free notification service that needs no account. The ntfy lesson explains it step by step.
- Install the ntfy app from Google Play or the App Store.
- Make up a topic name that nobody could guess, such as
eementor-fall-8q2m5t. Use only letters, numbers, - and _. - In the app, tap +, type your topic name and tap Subscribe. Everyone who should get the messages subscribes to the same topic on their own phone.
- Test it: open
https://ntfy.sh/your-topic-namein a browser, type a message and send it. It should pop up on the phones.
Important: on the free ntfy.sh server, anyone who knows your topic name can read your messages. Keep the name long and random, and never send personal details through it.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Put your topic in TOPIC and the person's name in NAME. Everything else is built into the ESP32 board package.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Wire.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TOPIC = "YOUR_SECRET_TOPIC"; // for example eementor-fall-8q2m5t
const char* NAME = "Grandpa"; // who wears the detector
const float IMPACT_G = 2.5; // a hit harder than this may be a fall
const float LYING_ANGLE = 50; // tipped further than this from standing = lying
const unsigned long CHECK_MS = 5000; // how long to watch for stillness after the hit
const unsigned long COUNTDOWN_MS = 30000; // time to press "I'm OK" before the family is told
const int MPU = 0x68, BUTTON_PIN = 32, BUZZER_PIN = 33, LED_PIN = 26;
enum State { WATCHING, SETTLING, CHECKING, COUNTDOWN, ALERTED };
State state = WATCHING;
unsigned long stateStart = 0, pressStart = 0, lastSample = 0, lastBeep = 0, lastWifiTry = 0, lastSendTry = 0;
float upX = 0, upY = 0, upZ = 1; // the direction of gravity while standing
float peak = 0;
int samples = 0, moving = 0;
bool lastButton = HIGH, longDone = false;
// Messages wait here until they have been sent
struct Message { bool waiting; String title, text, priority, tags; };
Message outbox[3];
enum { FALL, HELP, OKAY };
void queue(int slot, String title, String text, String priority, String tags) {
outbox[slot] = {true, title, text, priority, tags};
lastSendTry = 0; // try straight away
}
bool send(Message& m) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.setTimeout(5000);
http.begin(client, String("https://ntfy.sh/") + TOPIC);
http.addHeader("Title", m.title);
http.addHeader("Priority", m.priority);
http.addHeader("Tags", m.tags);
int code = http.POST(m.text);
http.end();
Serial.printf("ntfy answered %d\n", code);
return code == 200;
}
void readAccel(float& x, float& y, float& z) {
Wire.beginTransmission(MPU);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6);
int16_t v[3];
for (int i = 0; i < 3; i++) {
v[i] = Wire.read() << 8;
v[i] |= Wire.read();
}
x = v[0] / 4096.0; // in g (8 g range: 4096 steps per g)
y = v[1] / 4096.0;
z = v[2] / 4096.0;
}
void rememberStanding() { // stand up straight and keep still
float sx = 0, sy = 0, sz = 0, x, y, z;
for (int i = 0; i < 100; i++) {
readAccel(x, y, z);
sx += x; sy += y; sz += z;
delay(10);
}
float len = sqrt(sx * sx + sy * sy + sz * sz);
upX = sx / len; upY = sy / len; upZ = sz / len;
tone(BUZZER_PIN, 1047, 80);
Serial.println("Standing position saved");
}
float tiltFromStanding(float x, float y, float z) {
float len = sqrt(x * x + y * y + z * z);
float c = (x * upX + y * upY + z * upZ) / len; // dot product = cosine of the angle
return acos(constrain(c, -1.0, 1.0)) * 180 / PI;
}
void setState(State s) {
state = s;
stateStart = millis();
}
void askForHelp() {
queue(HELP, String(NAME) + " needs help", String(NAME) + " held the button on the fall detector. Please call them now, or go and check.", "urgent", "sos");
setState(ALERTED);
Serial.println("Help requested");
}
void shortPress() {
if (state == COUNTDOWN || state == SETTLING || state == CHECKING) { // "I'm OK": nothing is sent
setState(WATCHING);
tone(BUZZER_PIN, 784, 100);
delay(130);
tone(BUZZER_PIN, 1047, 150);
Serial.println("Cancelled: OK");
} else if (state == ALERTED) { // the family was told: now tell them it's OK
queue(OKAY, String(NAME) + " is OK", String(NAME) + " pressed the button on the fall detector: they are OK now.", "high", "white_check_mark");
setState(WATCHING);
tone(BUZZER_PIN, 1047, 150);
} else { // WATCHING: a short press saves "standing"
rememberStanding();
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
Wire.begin(21, 22); // SDA, SCL
Wire.beginTransmission(MPU);
Wire.write(0x6B); // wake the sensor up
Wire.write(0);
Wire.endTransmission();
Wire.beginTransmission(MPU);
Wire.write(0x1C); // accelerometer range: up to 8 g,
Wire.write(0x10); // so hard hits are not cut off at 2 g
Wire.endTransmission();
delay(100);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.println("Fall detector starting: stand still");
rememberStanding();
}
void loop() {
unsigned long now = millis();
// Button: short press = I'm OK (or save "standing"), hold 3 s = help
bool button = digitalRead(BUTTON_PIN);
if (lastButton == HIGH && button == LOW) {
pressStart = now;
longDone = false;
}
if (button == LOW && !longDone && now - pressStart > 3000) {
longDone = true;
askForHelp();
}
if (lastButton == LOW && button == HIGH && !longDone && now - pressStart > 50) shortPress();
lastButton = button;
// 100 readings a second
if (now - lastSample >= 10) {
lastSample = now;
float x, y, z;
readAccel(x, y, z);
float g = sqrt(x * x + y * y + z * z); // the total force, whatever the direction
if (state == WATCHING && g > IMPACT_G) {
peak = g;
Serial.printf("Impact: %.1f g\n", g);
setState(SETTLING);
} else if (state == SETTLING) { // let the body come to rest for 2 s
if (g > peak) peak = g;
if (now - stateStart > 2000) {
samples = 0;
moving = 0;
setState(CHECKING);
}
} else if (state == CHECKING) { // lying still?
samples++;
if (fabs(g - 1.0) > 0.25) moving++;
if (now - stateStart > CHECK_MS) {
float tilt = tiltFromStanding(x, y, z);
Serial.printf("After the impact: tipped %.0f degrees, moving in %d of %d readings\n", tilt, moving, samples);
if (tilt > LYING_ANGLE && moving < samples / 5) {
setState(COUNTDOWN);
Serial.println("Possible fall! Press the button within 30 s if you are OK");
} else {
setState(WATCHING); // got up, or only sat down hard
}
}
} else if (state == COUNTDOWN && now - stateStart > COUNTDOWN_MS) {
queue(FALL, "Possible fall: " + String(NAME), String(NAME) + "'s fall detector felt a hard fall (" + String(peak, 1) + " g) and they have not pressed \"I'm OK\". Please call them now, or go and check.", "urgent", "rotating_light");
setState(ALERTED);
Serial.println("Family alerted");
}
}
// Light and sound
if (state == COUNTDOWN) {
digitalWrite(LED_PIN, (now / 150) % 2);
if (now - lastBeep > 1000) { // "Are you OK?" beeps, every second
lastBeep = now;
tone(BUZZER_PIN, 988, 200);
}
} else if (state == ALERTED) {
digitalWrite(LED_PIN, HIGH);
if (now - lastBeep > 10000) { // a reminder beep every 10 s: help is on the way
lastBeep = now;
tone(BUZZER_PIN, 660, 100);
}
} else {
digitalWrite(LED_PIN, LOW);
}
// Wi-Fi and the outbox
if (WiFi.status() != WL_CONNECTED && now - lastWifiTry > 30000) {
lastWifiTry = now;
WiFi.disconnect();
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
}
if (WiFi.status() == WL_CONNECTED && (lastSendTry == 0 || now - lastSendTry > 30000)) {
for (int i = 0; i < 3; i++) {
if (!outbox[i].waiting) continue;
lastSendTry = now;
if (send(outbox[i])) outbox[i].waiting = false;
else break; // no luck: try again in 30 s
}
}
}
Try it (safely)
Never test it by falling yourself. Test it with the detector on its own, on a bed or a thick cushion.
- Upload the sketch and open the Serial Monitor at 115200 baud. Hold the breadboard upright, the way it will be worn, and keep it still until you hear the short beep: "Standing position saved".
- Sit down quickly on a chair while holding it upright. You may see "Impact", but then "tipped 10 degrees": no alarm, because you are still upright.
- Now hold it upright about 30 cm above a bed and drop it so it lands on its side. Leave it lying still. After about 7 seconds the LED blinks and it beeps every second: "Are you OK?"
- Press the button: two happy notes, and nothing is sent. Drop it again and this time wait 30 seconds: the family's phones get an urgent message.
- Press the button once more to send "is OK", then hold it for 3 seconds to try the help call.
Your detector may need different settings than ours. If small bumps cause countdowns, raise IMPACT_G to 3. If your test drops are not noticed, look at the "Impact" numbers in the Serial Monitor and lower it a little.
What you should see

How it works

The accelerometer measures force in three directions. Standing still, it only feels gravity: 1 g pointing down. To get the total force no matter how the detector is turned, the sketch combines the three numbers like the long side of a triangle (Pythagoras, in 3D): sqrt(x² + y² + z²). Walking gives about 1.2 g, sitting down hard about 2 g, and hitting the floor 3 g or more. The sensor normally measures up to 2 g, so setup() switches it to its 8 g range so that hard hits are not cut off.
A hard hit alone is not enough: dropping onto the sofa or bumping a door can do that too. So the detector works through states: WATCHING for an impact, SETTLING for 2 seconds while the body comes to rest, then CHECKING for 5 seconds. A fall leaves a person lying and often still. To find "lying", the sketch saved the direction of gravity while standing. The dot product of that direction and the gravity direction now gives the cosine of the angle between them: about 0° standing, about 90° lying on the floor. "Still" means that fewer than one reading in five was more than 0.25 g away from plain gravity.
Even then it might be wrong, so it asks first: 30 seconds of beeps that anyone near can hear, and a button to say "I'm OK". This confirm before alarm step is in real fall detectors too; too many false alarms and families stop taking them seriously. Messages wait in an outbox until they really have been sent, so a short Wi-Fi gap doesn't lose an alert.
Try this next
- Add a "free fall" check: in a real fall the total force drops below 0.5 g for a moment before the impact. Requiring it can make the detector smarter, and you can see it on the Serial Plotter.
- Send a daily "the detector is working" message at noon, like in the daily check-in button, so the family notices if it has lost power.
- Make it quieter at night but louder during the countdown, or let it speak through a small speaker.
- Put it in a small box with the power bank and a belt clip, and ask the wearer how comfortable it is.
Troubleshooting
-
"Impact" appears with every step: the sensor is loose. Fix it firmly, then raise
IMPACT_G. - A drop on the bed never starts the countdown: look at the "tipped … degrees" line. If it is small, the standing position was saved while lying: hold it upright and press the button once to save it again.
- The numbers stay at 0 and nothing happens: check SDA on GPIO 21 and SCL on GPIO 22, and VCC on 3V3.
- 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.
- No message on the phone: look for "ntfy answered" in the Serial Monitor. 200 means it was sent: check the topic name in the app letter by letter. -1 means no internet; 429 means too many messages in a short time.