Sharjah, UAE GST
Home/ Projects/ Lesson 5: Fall-Detection Learning Prototype
Lesson 5 of 5 Automation Intermediate ◎ 45-60 min

Lesson 5: Fall-Detection Learning Prototype

Put it all together: a jolt or drop followed by staying tipped over starts a beeping 20-second countdown that the button cancels. A learning experiment on the bench - not a safety device.

ESP32 Fall-Detection Learning Prototype
One of 5 projects in the ESP32 Fall-Detection Learning Prototype — the parts below are from that kit.

Before you start

Time
45-60 min
Do these first
On your own
You can do this lesson on your own; an adult should check the test platform. Never attach the prototype to a person or a wheelchair.

Exact parts

PartHow many
Everything from Lessons 1-4 (the buzzer only if you have it)all
Light test platform and cushion (yours)

Not in the box

  • A small, light test platform (a box or book) and a cushion or foam pad (yours)

What you build and learn

  • A state machine: MONITORING, candidate, COUNTDOWN, ALERT, FAULT
  • Why one reading is not enough: a jolt AND staying tipped over
  • Fault handling: stopping clearly when a sensor is missing
  • Testing honestly: recording false alarms and missed events

At start it finds one OLED and one GY-521, then learns "upright" for 3 seconds (Lesson 2). If anything is missing it shows FAULT and stops, rather than pretending to work.

MONITORING reads the sensor 50 times a second. A total above 2.5 g or below 0.45 g (Lesson 3) makes a candidate.

Within 5 seconds the platform must stay tipped more than 55° from upright for 1.5 seconds. Only then does the 20-second countdown start, beeping once a second.

A press cancels the countdown. If nobody presses, LOCAL ALERT beeps (Lesson 4; silent without the buzzer) until the button acknowledges it. Nothing is sent anywhere: no Wi-Fi, no message, no call.

New words

State machine
A program that is always in one named state and moves between them by rules.
Candidate
A possible event: a jolt or drop that might be a fall, waiting to be confirmed.
False alarm
An alert when nothing happened.
Missed event
Nothing happened on screen when it should have.

What this project uses

Every item below is in the kit. Not in the box: A small, light test platform (a box or book) and a cushion or foam pad (yours).

Build this with

ESP32 Fall-Detection Learning Prototype

All 6 parts in one box, with this project and 4 more to build from them.

Price on request See the kit Request this kit
Need only these parts? Order them loose · from AED 143.21
Used in this project tick what you still need
Selected parts AED 143.21

One of each part. Packs (resistor assortment, jumper wires) already hold more than a single build needs. a padded test platform (yours): included in the kit, ask us for it loose. Backorder parts ship once they arrive. Delivery is AED 20, free on orders AED 100+.

Connections

Unplug USB before you change any wire. Pin names are the labels printed on the board.

Lesson 5: Fall-Detection Learning Prototype - connection diagram with labelled pins
Connection diagram, drawn from this lesson's pin plan - not a photo of a tested build. Tap to open it full size. The table below says the same thing in words.
Part / pinConnect toNote
OLED and GY-521 VCC / GND+ rail (3V3) / − railLessons 1-2
OLED and GY-521 SDA / SCLGPIO21 / GPIO22Shared I2C
ButtonGPIO26 and GNDLesson 1
Buzzer I/O / VCC / GNDGPIO27 / 3V3 / GNDLesson 4
On-board LED (if your board has one)GPIO2Nothing to wire

Step by step

One thing at a time. Tick nothing off until it matches the diagram.

  1. Check all the wiring from Lessons 1-4 against the diagram.Watch out: Always unplug the USB cable before you add, move or remove a wire.
  2. Tape the breadboard and the ESP32 to a light box or book: the test platform.Watch out: Never attach the prototype to a person or a wheelchair.
  3. Plug in USB, upload the Lesson 5 code and keep the platform still and upright for 3 seconds: KEEP STILL, then MONITORING.
  4. Test the button first: change FALL_DIAGNOSTIC to true, upload, and press the button - the countdown starts with beeps. Press again to cancel. Then set it back to false and upload again.
  5. Over a cushion, give the platform a sharp knock and tip it onto its side. Hold it there for 2 seconds: POSSIBLE EVENT and beeps.Watch out: Short, gentle tests on a padded surface only.
  6. Press the button to cancel. Then do it again and let the 20 seconds run out: LOCAL ALERT. Press to acknowledge.Watch out: Keep the buzzer away from your ears.
  7. Try ordinary bumps, slow tilts and a knock without tipping over. Write down every false alarm and every missed event.

Sketch for this project

Written for the board in the kit. Copy it into the Arduino IDE; change the pin numbers if you wire it differently.

lesson5_fall_detection.ino arduino
/*
  DIY KIT - ESP32 Fall-Detection Learning Prototype
  Lesson 5 of 5: Fall-Detection Learning Prototype (the main build)

  WIRING: everything from Lessons 1-4.
    OLED and GY-521: VCC -> 3V3, GND -> GND, SDA -> GPIO21, SCL -> GPIO22
    Button: GPIO26 and GND        Buzzer: I/O -> GPIO27, VCC -> 3V3, GND -> GND

  WHAT SHOULD HAPPEN
    KEEP STILL AND LEVEL for 3 seconds (it learns "upright"), then MONITORING.
    A jolt above 2.5 g or a drop below 0.45 g, followed by staying tipped more
    than 55 degrees for 1.5 seconds, starts a 20-second countdown with a beep
    every second. Press the button to cancel. If nobody does, LOCAL ALERT:
    the buzzer beeps until you press the button. Nothing is sent anywhere.
    Serial Monitor at 115200 baud.

    Set FALL_DIAGNOSTIC to true to start the countdown with the button
    instead of a movement - handy for testing the screen and buzzer.

  An educational bench prototype. NOT a medical, wheelchair, emergency or
  life-safety device. Test only on a padded, unoccupied platform.
*/

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#ifndef FALL_DIAGNOSTIC
#define FALL_DIAGNOSTIC false
#endif

constexpr uint8_t SDA_PIN = 21;
constexpr uint8_t SCL_PIN = 22;
constexpr uint8_t BUTTON_PIN = 26;
constexpr uint8_t BUZZER_PIN = 27;  // Lesson 4
constexpr uint8_t LED_PIN = 2;  // Common ESP32 Dev Module onboard LED; verify board.
// Most buzzer modules beep when the pin is HIGH. If yours beeps in the gaps
// or all the time, change false to true (see Lesson 4).
constexpr bool BUZZER_ACTIVE_LOW = false;
constexpr uint32_t SAMPLE_MS = 20;
constexpr uint32_t DEBOUNCE_MS = 35;
constexpr uint32_t CANDIDATE_MS = 5000;
constexpr uint32_t TILT_HOLD_MS = 1500;
constexpr uint32_t COUNTDOWN_MS = 20000;
constexpr uint32_t DISPLAY_MS = 250;
constexpr float IMPACT_G = 2.5f;
constexpr float FREEFALL_G = 0.45f;
constexpr float TILT_DEG = 55.0f;

Adafruit_SSD1306 display(128, 64, &Wire, -1, 100000, 100000);

enum class State { Monitoring, Candidate, Countdown, Alert, Fault };
State state = State::Fault;
uint8_t mpuAddress = 0, oledAddress = 0;
float baseX = 0, baseY = 0, baseZ = 1;
uint32_t stateSince = 0, tiltSince = 0, lastSample = 0, lastDisplay = 0;
uint32_t events = 0;
uint8_t readFailures = 0;
bool rawButton = HIGH, stableButton = HIGH, buttonArmed = false;
uint32_t rawSince = 0;

void buzzer(bool on) {
  if (BUZZER_ACTIVE_LOW) digitalWrite(BUZZER_PIN, on ? LOW : HIGH);
  else                   digitalWrite(BUZZER_PIN, on ? HIGH : LOW);
}

bool elapsed(uint32_t now, uint32_t since, uint32_t interval) {
  return uint32_t(now - since) >= interval;
}

bool i2cAck(uint8_t address) {
  Wire.beginTransmission(address);
  return Wire.endTransmission() == 0;
}

bool writeRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(mpuAddress);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

bool readAccel(float &x, float &y, float &z) {
  Wire.beginTransmission(mpuAddress);
  Wire.write(0x3B);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(int(mpuAddress), 6) != 6) return false;
  int16_t rx = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
  int16_t ry = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
  int16_t rz = int16_t((uint16_t(Wire.read()) << 8) | Wire.read());
  x = rx / 4096.0f;
  y = ry / 4096.0f;
  z = rz / 4096.0f;
  return true;
}

void show(const __FlashStringHelper *line1, const String &line2 = "") {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("FALL-DETECTION LAB"));
  display.println(F("TRAINING ONLY"));
  display.println();
  display.println(line1);
  display.println(line2);
  display.display();
}

void fault(const __FlashStringHelper *reason) {
  state = State::Fault;
  buzzer(false);
  digitalWrite(LED_PIN, HIGH);
  Serial.print(F("FAULT: "));
  Serial.println(reason);
  if (oledAddress && i2cAck(oledAddress)) show(F("FAULT - POWER OFF"), String(reason));
}

bool initialiseDevices() {
  bool oled3c = i2cAck(0x3C), oled3d = i2cAck(0x3D);
  if (oled3c == oled3d) return false;
  oledAddress = oled3c ? 0x3C : 0x3D;
  if (!display.begin(SSD1306_SWITCHCAPVCC, oledAddress, true, false)) return false;

  bool mpu68 = i2cAck(0x68), mpu69 = i2cAck(0x69);
  if (mpu68 == mpu69) return false;
  mpuAddress = mpu68 ? 0x68 : 0x69;
  if (!writeRegister(0x6B, 0x01)) return false;
  delay(50);
  if (!writeRegister(0x1C, 0x10)) return false;
  if (!writeRegister(0x1A, 0x04)) return false;
  Serial.printf("OLED=0x%02X MPU=0x%02X\n", oledAddress, mpuAddress);
  return true;
}

bool calibrateBaseline() {
  show(F("KEEP STILL AND LEVEL"), F("Calibrating 3 seconds"));
  float sx = 0, sy = 0, sz = 0;
  constexpr int samples = 150;
  for (int i = 0; i < samples; ++i) {
    float x, y, z;
    if (!readAccel(x, y, z)) return false;
    float mag = sqrtf(x * x + y * y + z * z);
    if (mag < 0.75f || mag > 1.25f) return false;
    sx += x; sy += y; sz += z;
    delay(20);
  }
  float mag = sqrtf(sx * sx + sy * sy + sz * sz);
  if (mag < 0.001f) return false;
  baseX = sx / mag; baseY = sy / mag; baseZ = sz / mag;
  return true;
}

float angleFromBaseline(float x, float y, float z, float mag) {
  if (mag < 0.001f) return 180.0f;
  float dot = (x * baseX + y * baseY + z * baseZ) / mag;
  return acosf(constrain(dot, -1.0f, 1.0f)) * 180.0f / PI;
}

bool pressedEdge(uint32_t now) {
  bool reading = digitalRead(BUTTON_PIN);
  if (reading != rawButton) {
    rawButton = reading;
    rawSince = now;
  }
  bool edge = false;
  if (reading != stableButton && elapsed(now, rawSince, DEBOUNCE_MS)) {
    stableButton = reading;
    edge = stableButton == LOW;
    Serial.println(edge ? F("Button pressed") : F("Button released"));
  }
  if (stableButton == HIGH && rawButton == HIGH && elapsed(now, rawSince, DEBOUNCE_MS))
    buttonArmed = true;
  if (edge && buttonArmed) {
    buttonArmed = false;
    return true;
  }
  return false;
}

void enterCountdown(uint32_t now, const __FlashStringHelper *source) {
  state = State::Countdown;
  stateSince = now;
  ++events;
  Serial.print(F("Countdown started: "));
  Serial.println(source);
  show(F("EVENT? 20 SECONDS"), F("Button = cancel"));
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  pinMode(BUZZER_PIN, OUTPUT);
  buzzer(false);
  rawButton = stableButton = digitalRead(BUTTON_PIN);
  rawSince = millis();
  if (!Wire.begin(SDA_PIN, SCL_PIN, 100000)) {
    fault(F("I2C start failed"));
    return;
  }
  Wire.setTimeOut(20);
  if (!initialiseDevices()) {
    fault(F("Need one OLED and one MPU"));
    return;
  }
  if (!calibrateBaseline()) {
    fault(F("Calibration failed"));
    return;
  }
  state = State::Monitoring;
  stateSince = millis();
  show(F("MONITORING"), FALL_DIAGNOSTIC ? F("Button starts test") : F("Local demo only"));
  Serial.println(F("Local demo only: no Wi-Fi, no messages, no emergency call."));
}

void loop() {
  uint32_t now = millis();
  bool button = pressedEdge(now);
  if (state == State::Fault) return;

  if (button && state == State::Countdown) {
    state = State::Monitoring;
    stateSince = now;
    digitalWrite(LED_PIN, LOW);
    buzzer(false);
    show(F("CANCELLED"), F("Monitoring resumed"));
    Serial.println(F("Countdown cancelled"));
    return;
  }
  if (button && state == State::Alert) {
    state = State::Monitoring;
    stateSince = now;
    digitalWrite(LED_PIN, LOW);
    buzzer(false);
    show(F("ACKNOWLEDGED"), F("Monitoring resumed"));
    Serial.println(F("Local alert acknowledged"));
    return;
  }
  if (button && FALL_DIAGNOSTIC && state == State::Monitoring) {
    enterCountdown(now, F("diagnostic button"));
    return;
  }

  if (state == State::Countdown) {
    uint32_t used = now - stateSince;
    if (used >= COUNTDOWN_MS) {
      state = State::Alert;
      stateSince = now;
      buzzer(false);
      show(F("LOCAL ALERT"), F("Press to acknowledge"));
      Serial.println(F("LOCAL ALERT ONLY - no message was sent"));
      return;
    }
    digitalWrite(LED_PIN, ((now / 300) & 1) ? HIGH : LOW);
    buzzer((used % 1000) < 80);  // a short beep at the start of every second
    if (elapsed(now, lastDisplay, DISPLAY_MS)) {
      lastDisplay = now;
      uint32_t seconds = (COUNTDOWN_MS - used + 999) / 1000;
      show(F("POSSIBLE EVENT"), String(seconds) + F(" s - button cancels"));
    }
    return;
  }
  if (state == State::Alert) {
    digitalWrite(LED_PIN, ((now / 150) & 1) ? HIGH : LOW);
    buzzer(((now - stateSince) % 500) < 150);  // 150 ms on, 350 ms off - as in Lesson 4
    return;
  }
  if (!elapsed(now, lastSample, SAMPLE_MS)) return;
  lastSample = now;

  float x, y, z;
  if (!readAccel(x, y, z)) {
    if (++readFailures >= 5) fault(F("MPU read failed"));
    return;
  }
  readFailures = 0;
  float mag = sqrtf(x * x + y * y + z * z);
  float tilt = angleFromBaseline(x, y, z, mag);

  if (state == State::Monitoring) {
    if (mag > IMPACT_G || mag < FREEFALL_G) {
      state = State::Candidate;
      stateSince = now;
      tiltSince = 0;
      Serial.printf("Candidate: %.2f g, %.1f deg\n", mag, tilt);
    } else if (elapsed(now, lastDisplay, 1000)) {
      lastDisplay = now;
      show(F("MONITORING"), String(mag, 2) + F(" g  events ") + events);
    }
    return;
  }

  if (state == State::Candidate) {
    if (elapsed(now, stateSince, CANDIDATE_MS)) {
      state = State::Monitoring;
      tiltSince = 0;
      show(F("NO EVENT"), F("Monitoring resumed"));
      return;
    }
    if (tilt >= TILT_DEG) {
      if (tiltSince == 0) tiltSince = now;
      if (elapsed(now, tiltSince, TILT_HOLD_MS)) enterCountdown(now, F("motion rule"));
    } else {
      tiltSince = 0;
    }
  }
}
ℹCompiler-checked for ESP32 Dev Module (ESP32 core 3.3.11, Adafruit SSD1306 2.5.17 / GFX 1.12.6 / BusIO 1.17.4, 0 warnings) and run in the kit simulator. Not yet bench-tested on the shipped parts.

Upload it

  1. Click Copy (or Download .ino and open the file).
  2. In the Arduino IDE choose File → New Sketch, select everything in it and paste.
  3. Check the bottom of the window says Arduino Uno and your port (Start here, step 5).
  4. Click Upload (the → arrow) and wait for Done uploading.

How the code works

In the codeWhat it does
enum class State { Monitoring, Candidate, Countdown, Alert, Fault };The five states the prototype can be in.
if (mag > IMPACT_G || mag < FREEFALL_G)Step 1 of the rule: a jolt or a drop makes a candidate.
if (tilt >= TILT_DEG) ... TILT_HOLD_MSStep 2: it must stay tipped past 55° for 1.5 seconds within CANDIDATE_MS.
buzzer((used % 1000) < 80);A short beep at the start of every second of the countdown.
fault(...)Stops in a clear FAULT state when a device is missing or reads fail 5 times in a row.
#define FALL_DIAGNOSTIC falsetrue = the button starts a countdown, to test the screen and buzzer without moving anything.

Change it and see

Change: Change TILT_HOLD_MS from 1500 to 3000.What happens: It must stay tipped over longer - fewer false alarms, but slower.
Change: Change COUNTDOWN_MS from 20000 to 10000.What happens: A 10-second countdown.

What you should see or hear

  • KEEP STILL AND LEVEL, then MONITORING with the total g and an event count.
  • A knock without tipping over: NO EVENT after 5 seconds.
  • Knock and tip over, held 1.5 s: POSSIBLE EVENT, a countdown and a beep every second. A press: CANCELLED.
  • No press for 20 s: LOCAL ALERT, beeping until a press: ACKNOWLEDGED. A slow tilt alone does nothing.

If it doesn't work

ProblemWhat to check
"FAULT - POWER OFF" and "Need one OLED and one MPU"One device did not answer. Unplug USB and check both on GPIO21/22 and 3V3 - see Lessons 1 and 2.
"FAULT - POWER OFF" and "Calibration failed"It moved during the 3 seconds, or the sensor is loose. Keep it still and press the EN (reset) button on the ESP32.
It never starts a countdownThe knock must cross 2.5 g (or a drop 0.45 g) AND it must stay tipped past 55° for 1.5 s. Check with Lesson 3 what your knocks reach.
Countdowns start by themselvesLoose mounting or wires. Tape everything down. Record it - false alarms are part of the lesson.
It beeps in the gaps, or all the timeSet BUZZER_ACTIVE_LOW to true (see Lesson 4).
The upload fails or stops at "Connecting..."See Start here, step 6.

Small experiment

Run 10 knock-and-tip tests and 20 ordinary handling tests. Make a table: how many real events did it catch, how many did it miss, and how many false alarms? Change one threshold and repeat.

Check what you learned

Why does the rule need a jolt AND staying tipped over?

A jolt alone happens all the time (bumps); tipped alone happens when you lay it down. Together they are more like a fall.

What happens when the GY-521 stops answering?

After 5 failed reads it goes to FAULT and stops, instead of pretending to monitor.

Why is this not a safety device?

It is untested on real falls, it misses events and gives false alarms, and it does not call anyone.

▶In the simulator: In the simulator set g above 2.5 for a moment, then tilt past 55° - the countdown starts. Real movements are noisy and much harder to classify.

Next

  • Ideas: log every event to the Serial Monitor with the time, add a second button to pause monitoring, or show the peak g on the OLED.
  • Remote alerts over Wi-Fi are a separate project: they need secure messaging, a plan for when the network fails, and a person who always responds. This kit does not send anything.

Stuck?

Most problems are one wire in the wrong hole. Check these first:

  1. Unplug, then compare every wire with the lesson's diagram and table. One pin off is the most common problem.
  2. Are all VCC wires on 3V3 (not 5V or VIN), and all GND wires on GND?
  3. OLED and GY-521: SDA on GPIO21 and SCL on GPIO22, by the labels on each module?
  4. Serial Monitor: is it set to 115200 baud?
  5. Upload problems: Tools → Board = ESP32 Dev Module, the right port, and hold BOOT if it stops at "Connecting". (Start here, step 6.)
Still stuck? Ask us on WhatsApp. Send this message with a photo of your wiring - it tells us exactly where you are.
Hi DIY KIT, I'm stuck on the ESP32 Fall-Detection Learning Prototype.
Lesson: __
Step number: __
What should happen: __
What happens instead: __
Error message (copied exactly): __
I'll attach a photo of my wiring.
Message us on WhatsApp

More from this kit

All projects →

Bot
ECHO
Online — usually replies instantly
Catalog assistant: verify final stock/price on the product page before checkout.