Siddhant Kumar
Project 016 · Health & Wearables

Fall Detection Pendant.

A pendant that distinguishes an actual fall from sitting down heavily, gives the wearer thirty seconds to cancel, and then escalates to family with a location — running for months on one charge.

Intermediate 12–18 hours 41 min read IMUElderlyAlerts
Jump to source Bill of materials
Fall Detection Pendant — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
12–18 hours
Indicative cost
₹2,200 – ₹2,900
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Health & Wearables
Last updated
28 July 2026
Contents — 26 sections

Project Overview

A pendant that distinguishes an actual fall from sitting down heavily, gives the wearer thirty seconds to cancel, and then escalates to family with a location — running for months on one charge.

Fall detection is a classification problem with a brutally asymmetric cost function. A missed fall can mean hours on a floor; a false alarm at 2 a.m. means a frightened family and, after the third one, a device that gets taken off and left in a drawer. Almost every published fall-detection algorithm optimises sensitivity and quietly ignores what its false-positive rate does to real-world adoption.

This design uses the three-phase signature that distinguishes a genuine fall from every common false positive. A real fall has a free-fall phase where total acceleration drops well below 1 g, followed within a few hundred milliseconds by an impact spike, followed by a period of altered orientation and near-stillness. Sitting down heavily has the impact but no free-fall. Dropping the pendant has free-fall and impact but the orientation afterwards is random and it is picked up quickly. Requiring all three phases in sequence, with timing constraints, removes most false positives without sacrificing much sensitivity.

The other half of the design is the escalation protocol, which matters as much as the detection. On a detected fall the pendant vibrates and beeps for thirty seconds with a large cancel button — because most detected events are recoverable and the wearer is fine. Only on timeout does it alert, and then it alerts progressively: a family member first, then a second contact, then a broader group, each with the previous alert's outcome attached.

Power is the constraint that shapes everything else. A pendant that needs charging nightly gets left off the charger. The design targets months, which means the accelerometer's own interrupt engine does the watching while the microcontroller sleeps at microamps, waking only when something happens.

A wrist-worn fitness tracker
A wrist-worn wearable. A pendant form factor is generally preferred for fall detection because chest-level mounting gives a cleaner orientation signal than a wrist. Photograph sourced from Wikimedia Commons — Fitness tracker.jpg. Reused under the licence stated on that page; please check it before republishing.

What this project does

  • Detects falls using a three-phase free-fall, impact and post-impact orientation signature.
  • Rejects sitting, lying down, dropping the device and normal walking through explicit timing and orientation tests.
  • Gives a thirty-second cancel window with escalating vibration and audible prompts.
  • Escalates through a contact list, attaching the outcome of each previous stage.
  • Includes a manual SOS button that bypasses detection entirely.
  • Reports approximate location from Wi-Fi access-point scanning without needing GPS.
  • Runs for two to four months on a 500 mAh cell through hardware-interrupt wake.

Real-World Applications

SettingHow it is used
Independent elderly livingThe core use case, and the one where the false-positive rate determines whether the device is actually worn.
Epilepsy and syncope monitoringThe same signature detects a collapse from loss of consciousness.
Lone worker safetyIndustrial man-down detection, often a regulatory requirement, with the same algorithm and a rugged enclosure.
Post-surgical recovery at homeA temporary safety net during the weeks when balance is impaired.
Care home monitoringMany pendants, one gateway, with per-resident alerting.
Teaching sensor fusion and classificationA real problem where a naive threshold demonstrably fails and the reason why is physically intuitive.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Three-phase detection with timing constraints between phases, not a single acceleration threshold.
  • ADXL345 activity and free-fall interrupts so the MCU sleeps at about 12 µA until something happens.
  • Orientation change test using the gravity vector before and after impact.
  • Post-impact stillness window — a person who gets straight up did not need help.
  • Thirty-second cancel with a large, findable button and escalating haptics.
  • Multi-stage escalation with per-contact acknowledgement.
  • Wi-Fi RSSI fingerprint location, accurate to a room indoors, with no GPS power cost.
  • Daily heartbeat with battery voltage, so a dead pendant is visibly dead.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time12–18 hours
Indicative build cost₹2,200 – ₹2,900
Primary disciplineHealth & Wearables
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Arduino C++ with interrupt handling and deep sleep
  • Understanding of accelerometer axes, gravity and dynamic acceleration
  • Configuring sensor hardware interrupts through registers
  • Simple state machines
  • MQTT and notification services

Bill of Materials

Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift — treat them as a budgeting guide, not a quotation.

ComponentKey specificationQtyApprox. cost
ESP32 DevKit V1 (ESP-WROOM-32)
Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC1₹450
ADXL345 3-axis accelerometer
Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz1₹220
Active piezo buzzer 5 V
Active buzzers make tone on DC; passive ones need a PWM carrier.
85 dB at 10 cm, 2.3 kHz resonance, 12 mm diameter1₹25
18650 Li-ion cell 3400 mAh + holder
Never charge below 0 °C; always use a protected cell or a BMS.
3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge1₹450
TP4056 Li-ion charger + DW01 protection
Buy the version *with* protection ICs — the bare charger will over-discharge your cell.
1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection1₹45
Double-sided perfboard 7 × 9 cm + headers
Solder female headers so the MCU can be swapped without desoldering.
FR-4, 0.1″ pitch, plated through-holes, 24 × 18 grid1₹60
Coin vibration motor + driver transistor10 mm, 3 V, 80 mA1₹120
Large tactile SOS / cancel button
Must be findable by touch alone, by someone who has just fallen and may be disoriented.
12 mm cap, NO, high travel1₹60
3D-printed pendant shell + lanyard
The lanyard must break away under load — a fixed cord around a neck is a strangulation risk.
PETG, break-away lanyard clasp1₹180
500 mAh LiPo cell3.7 V, with protection circuit1₹260

Estimated total: ₹1,870, excluding tools, shipping and consumables.

Tools and consumables

  • Soldering iron (temperature controlled, 350 °C) with 0.8 mm 60/40 or lead-free solder
  • Digital multimeter — continuity, DC volts and current ranges
  • Wire strippers, flush cutters and a small set of precision screwdrivers
  • Heat-shrink tubing and a heat gun (or a lighter, carefully)
  • A laptop with a USB port and the toolchain listed above

Hardware Specifications

PartSpecificationSupplyInterfaceReference
ESP32 DevKit V1 (ESP-WROOM-32)Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18× 12-bit ADC, 2× 8-bit DAC3.3 V logic / 5 V USBUART, SPI, I²C, I²S, CAN, PWMDatasheet
ADXL345 3-axis accelerometer±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz2.0–3.6 VI²C / SPIDatasheet
Active piezo buzzer 5 V85 dB at 10 cm, 2.3 kHz resonance, 12 mm diameter3–5 VDigital / PWMDatasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet
TP4056 Li-ion charger + DW01 protection1 A programmable CC/CV charge to 4.2 V ±1 %, over-discharge and short protection4.5–5.5 V inmicro-USB / padsDatasheet
Double-sided perfboard 7 × 9 cm + headersFR-4, 0.1″ pitch, plated through-holes, 24 × 18 gridDatasheet

Consolidated electrical and interface specifications for every active part in the build.

Power Budget & Supply Sizing

Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA — size the regulator accordingly.
ADXL345 3-axis accelerometer2.0–3.6 V0.14Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
Active piezo buzzer 5 V3–5 V30Active buzzers make tone on DC; passive ones need a PWM carrier.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy the version *with* protection ICs — the bare charger will over-discharge your cell.

Summed typical draw is 1190.1399999999999 mA. With a 1.5× design margin the supply should deliver at least 1800 mA continuously at the stated rail voltage.

Software Requirements & Development Environment

Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.

  • Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
  • Add https://espressif.github.io/arduino-esp32/package_esp32_index.json under File → Preferences → Additional Board Manager URLs, then install esp32 from the Boards Manager.
  • Set the correct port under Tools → Port. On Linux add yourself to the dialout group: sudo usermod -aG dialout $USER and log out and back in.
  • Open the Serial Monitor at 115200 baud — every sketch here logs its state there.
  • Keep File → Preferences → Show verbose output during: compilation switched on while you are debugging build errors.

Required libraries

LibraryWhy it is neededInstall
WiFi (ESP32 core) bundledStation/AP connection management for the ESP32.Bundled with the ESP32 Arduino core
PubSubClient 2.8Lightweight MQTT 3.1.1 client for constrained devices.Library Manager → "PubSubClient" by Nick O'Leary
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Preferences (NVS) bundledWear-levelled key/value storage in ESP32 flash for settings.Bundled with the ESP32 core

Block Diagram

The block diagram shows the functional decomposition of the system — what senses, what decides, what acts, and where the data ends up.

Fall Detection Pendant — system block diagramFunctional block diagram of the Fall Detection Pendant system. WatchADXL345HW free-fall INTESP32 deep sleep~12 µAClassifyPhase 1 free-fall< 0.6 gPhase 2 impact> 2.5 gPhase 3 stillnessorientation changedConfirm30 s cancel windowhaptic + audibleButton pressed?user is fineEscalateContact 1 → 2 → allwith locationWi-Fi AP scanroom-level fixwake on eventfall candidateno cancel
Fall Detection Pendant — system block diagram

Circuit Diagram & Wiring

Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.

Fall Detection Pendant — wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBADXL345 accelerometerGPIO 21 / 22I²C at 0x53ADXL345 INT1GPIO 33Free-fall /activity, RTC wakeSOS / cancel buttonGPIO 32Pull-up, also an RTCwake sourceBattery dividerGPIO 341 MΩ / 1 MΩ, MOSFETgatedVibration motorGPIO 25Through 1 kΩ, withflybackPiezo buzzerGPIO 26LEDC PWMStatus LEDGPIO 27Brief flashes only
Fall Detection Pendant — wiring schematic
PeripheralPeripheral pinController pinSignal
ADXL345 accelerometerSDA / SCLGPIO 21 / 22I²C at 0x53
ADXL345 INT1INT1GPIO 33Free-fall / activity, RTC wake
SOS / cancel buttonNOGPIO 32Pull-up, also an RTC wake source
Battery dividerMid-pointGPIO 341 MΩ / 1 MΩ, MOSFET gated
Vibration motorTransistor baseGPIO 25Through 1 kΩ, with flyback
Piezo buzzer+GPIO 26LEDC PWM
Status LEDAnodeGPIO 27Brief flashes only

Wire one row at a time and tick it off — most "it does not work" reports trace back to a single swapped pair.

Wiring explanation

  • Wire INT1 to an RTC-capable GPIO (33 here) so it can wake the ESP32 from deep sleep via ext0. A non-RTC pin means the pendant sleeps and never wakes, which is a silent and total failure.
  • Configure the ADXL345 interrupt as active high and wake on high. The default is active low, which conflicts with the pull-up arrangement used for the button on the same wake mechanism.
  • The vibration motor is inductive and draws about 80 mA. Drive it through an NPN transistor or a small MOSFET with a flyback diode — a GPIO cannot supply it and the back-EMF will damage the pin.
  • The lanyard clasp must break away under roughly 3 kg of load. A pendant on a fixed cord is a genuine strangulation hazard, particularly for the population this device is for.
  • Mount the accelerometer rigidly to the shell. A sensor that can rattle inside the case produces impact spikes from ordinary handling.
  • Use a bare ESP32 module rather than a development board. A DevKit draws 8–20 mA in deep sleep because of its regulator and USB chip, which turns three months of battery life into three days.

System Architecture

Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.

Fall Detection Pendant — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · sensors and actuatorsDriver layerwifi · pubsub · arduinojson · preferencesApplication logicsampling loop · filtering · thresholds · state machineTransport layerWi-Fi + MQTT · TLS · retry and backoffPresentation layerdashboard · mobile notifications · historical charts
Fall Detection Pendant — architecture stack

Working Principle

An accelerometer at rest measures 1 g — not zero. It senses the normal force opposing gravity, so a stationary sensor reads 9.81 m/s² along whichever axis points up. This is the key to the whole algorithm: during genuine free fall, nothing opposes gravity, so the measured total acceleration drops towards 0 g. A body falling towards the floor is in partial free fall for 200–400 ms, and total acceleration typically dips to 0.3–0.7 g. Nothing else in daily life produces that signature.

The impact follows. Deceleration on hitting the floor produces a spike of 2–6 g depending on the surface and how the person lands. On its own this is a poor discriminator — sitting down heavily on a chair produces 2–3 g, and setting the pendant down on a table produces more. It is only the sequence free-fall then impact, with under 800 ms between them, that is specific to a fall.

The third phase is what removes the remaining false positives. After a real fall the person is on the floor, so the pendant's orientation relative to gravity has changed substantially — typically by more than 30° — and they are largely still for at least a couple of seconds. Someone who sits down heavily has a small orientation change and continues to move normally. Someone who drops the pendant picks it up within seconds. Requiring sustained stillness in a changed orientation is the single most effective false-positive filter available.

Orientation is computed from the gravity vector: with the device roughly still, the measured acceleration vector points along "up" in the device frame. Comparing the unit vector before the event with the one after gives the angle change directly through the dot product. This works without a gyroscope and without any integration, which matters because integrating accelerometer data to get orientation drifts badly within seconds.

The power architecture uses the ADXL345's own interrupt engine. The sensor has hardware free-fall and activity detection with configurable thresholds and time windows, running from its own 40 µA supply. It watches continuously while the ESP32 sleeps at around 10 µA. The microcontroller only wakes when the accelerometer says something happened, which is a handful of times a day, and each wake costs a few hundred milliamp-milliseconds.

The maths behind it

Free-fall and impact thresholds

plainFree-fall and impact thresholds
Total acceleration magnitude:
  |a| = sqrt(ax² + ay² + az²)

At rest        : |a| ≈ 1.00 g
Free fall      : |a| →  0 g  (in practice 0.3–0.7 g for a body)
Impact         : |a| =  2–6 g

ADXL345 free-fall register settings:
  THRESH_FF (0x28) = 0x08  → 8 × 62.5 mg = 0.50 g
  TIME_FF   (0x29) = 0x14  → 20 × 5 ms   = 100 ms

Requiring 100 ms below 0.5 g rejects the brief dips
from ordinary arm swing, which last under 40 ms.

Orientation change from the gravity vector

plainOrientation change from the gravity vector
Before: g₁ = (x₁, y₁, z₁) normalised
After : g₂ = (x₂, y₂, z₂) normalised

  cos θ = g₁ · g₂ = x₁x₂ + y₁y₂ + z₁z₂
  θ = acos(cos θ)

Example — pendant upright, then lying on its side:
  g₁ = (0.02, 0.05, 0.998)
  g₂ = (0.97, 0.11, 0.21)
  cos θ = 0.019 + 0.006 + 0.210 = 0.235
  θ = 76.4°   → clearly a fall-consistent change

Sitting down typically gives θ under 20°.

Battery life

plainBattery life
ADXL345 in measurement mode with interrupts: 40 µA
ESP32 deep sleep (bare module)              : 10 µA
Total idle                                  : 50 µA

Wakes: ~6 false triggers/day × 3 s at 90 mA
  = 6 × 3 × 90 / 86400 = 18.75 µA average
Daily heartbeat: 1 × 8 s at 130 mA
  = 8 × 130 / 86400 = 12.0 µA average

Total ≈ 81 µA

500 mAh cell, usable to 3.4 V ≈ 430 mAh
Life = 430 / 0.081 = 5309 h ≈ 7.4 months

Derate for self-discharge and cold: 3–5 months realistic.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Fall Detection Pendant — firmware flowchartControl flow through the main program loop. Deep sleep, ADXL345 watchingWake on free-fall interruptSample at 100 Hz for 3 sImpact > 2.5 g within 800ms?yesnot a fall, sleepOrientation changed by >30°?yesnot a fall, sleepStill for 2 s afterimpact?fall candidaterecovered, sleep30 s cancel window withhapticsEscalate with location, thensleep
Fall Detection Pendant — firmware flowchart

Assembly Instructions

Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.

Step-by-Step Implementation Guide

Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.

Complete Source Code

The listing below is complete and compiles as written — there are no elided sections. Read the annotations under each block before you upload it.

cppfall-pendant.ino
/* ═══════════════════════════════════════════════════════════════
   Fall Detection Pendant — ESP32 + ADXL345

   Three-phase detection: free fall, impact, then a sustained change
   in orientation with stillness. The accelerometer's own interrupt
   engine watches while the ESP32 sleeps at ~10 uA.

   Not a medical device. A supplement to human care, never a
   replacement for it.
   ══════════════════════════════════════════════════════════════════ */

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <esp_sleep.h>
#include <driver/rtc_io.h>
#include <math.h>

#define WIFI_SSID "YOUR_WIFI"
#define WIFI_PASS "YOUR_PASSWORD"
#define MQTT_HOST "192.168.1.50"
#define DEVICE_ID "pendant-01"

#define ADXL_ADDR 0x53
#define PIN_INT   GPIO_NUM_33
#define PIN_BTN   GPIO_NUM_32
#define PIN_VIBE  25
#define PIN_BUZZ  26
#define PIN_LED   27
#define PIN_BATT  34
#define PIN_BATT_EN 14

#define FF_THRESH_G     0.60f
#define IMPACT_THRESH_G 2.50f
#define IMPACT_WINDOW_MS 800
#define ORIENT_CHANGE_DEG 30.0f
#define STILL_THRESH_G  0.18f
#define STILL_WINDOW_MS 2000
#define CANCEL_WINDOW_S  30
#define HEARTBEAT_S  86400ULL

RTC_DATA_ATTR uint32_t bootCount = 0, fallCount = 0, cancelCount = 0;

WiFiClient   net;
PubSubClient mqtt(net);
volatile bool acknowledged = false;

/* ── ADXL345 ────────────────────────────────────────────────── */
void adxlWrite(uint8_t reg, uint8_t val) {
  Wire.beginTransmission(ADXL_ADDR);
  Wire.write(reg); Wire.write(val);
  Wire.endTransmission();
}

uint8_t adxlRead(uint8_t reg) {
  Wire.beginTransmission(ADXL_ADDR);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(ADXL_ADDR, 1);
  return Wire.read();
}

void adxlReadXYZ(float &x, float &y, float &z) {
  Wire.beginTransmission(ADXL_ADDR);
  Wire.write(0x32);                     // DATAX0
  Wire.endTransmission(false);
  Wire.requestFrom(ADXL_ADDR, 6);
  int16_t rx = Wire.read() | (Wire.read() << 8);
  int16_t ry = Wire.read() | (Wire.read() << 8);
  int16_t rz = Wire.read() | (Wire.read() << 8);
  x = rx * 0.0039f; y = ry * 0.0039f; z = rz * 0.0039f;   // 3.9 mg/LSB
}

void adxlBegin() {
  adxlWrite(0x2D, 0x00);                // standby while configuring
  adxlWrite(0x31, 0x08);                // full resolution, +/-2 g
  adxlWrite(0x2C, 0x0A);                // 100 Hz output data rate

  // Hardware free-fall detection does the watching while we sleep.
  adxlWrite(0x28, 0x09);                // THRESH_FF: 9 x 62.5 mg = 0.56 g
  adxlWrite(0x29, 0x14);                // TIME_FF:  20 x 5 ms   = 100 ms
  adxlWrite(0x2E, 0x04);                // INT_ENABLE: FREE_FALL only
  adxlWrite(0x2F, 0x00);                // map all interrupts to INT1
  adxlWrite(0x31, adxlRead(0x31) & ~0x20);   // INT active HIGH

  adxlWrite(0x2D, 0x08);                // measurement mode
  adxlRead(0x30);                       // clear any pending interrupt
}

/* ── feedback ───────────────────────────────────────────────── */
void vibe(uint16_t ms) { digitalWrite(PIN_VIBE, HIGH); delay(ms); digitalWrite(PIN_VIBE, LOW); }

void alertPattern(uint8_t intensity) {
  // Escalates as the cancel window runs down.
  for (uint8_t i = 0; i <= intensity; i++) {
    tone(PIN_BUZZ, 2000 + intensity * 300, 150);
    vibe(160);
    delay(120);
  }
}

/* ── battery ────────────────────────────────────────────────── */
float batteryVolts() {
  pinMode(PIN_BATT_EN, OUTPUT);
  digitalWrite(PIN_BATT_EN, HIGH);
  delay(5);
  uint32_t acc = 0;
  for (int i = 0; i < 16; i++) { acc += analogRead(PIN_BATT); delay(2); }
  digitalWrite(PIN_BATT_EN, LOW);
  return (acc / 16.0f / 4095.0f) * 3.3f * 2.0f * 1.04f;
}

/* ── the three-phase classifier ─────────────────────────────── */
struct FallResult {
  bool     isFall;
  float    peakG, orientationDeg;
  uint16_t impactDelayMs;
  const char *rejectedBecause;
};

FallResult classify() {
  FallResult r = { false, 0, 0, 0, nullptr };

  float x, y, z;
  adxlReadXYZ(x, y, z);
  float mag = sqrtf(x * x + y * y + z * z);

  // --- Phase 1: confirm the free fall that woke us ---------------
  uint32_t t0 = millis();
  bool sawFreeFall = false;
  while (millis() - t0 < 400) {
    adxlReadXYZ(x, y, z);
    mag = sqrtf(x * x + y * y + z * z);
    if (mag < FF_THRESH_G) { sawFreeFall = true; break; }
    delay(5);
  }
  if (!sawFreeFall) { r.rejectedBecause = "no-freefall"; return r; }

  // --- Phase 2: impact within the window -------------------------
  uint32_t ffEnd = millis();
  float g1x = 0, g1y = 0, g1z = 0;
  while (millis() - ffEnd < IMPACT_WINDOW_MS) {
    adxlReadXYZ(x, y, z);
    mag = sqrtf(x * x + y * y + z * z);
    if (mag > r.peakG) { r.peakG = mag; }
    if (mag > IMPACT_THRESH_G) { r.impactDelayMs = millis() - ffEnd; break; }
    delay(5);
  }
  if (r.peakG < IMPACT_THRESH_G) { r.rejectedBecause = "no-impact"; return r; }

  delay(300);                                  // let the ringing settle
  adxlReadXYZ(g1x, g1y, g1z);                  // orientation just after impact

  // --- Phase 3: stillness in a changed orientation ---------------
  uint32_t stillStart = millis();
  float sumX = 0, sumY = 0, sumZ = 0;
  int n = 0;
  bool moved = false;
  while (millis() - stillStart < STILL_WINDOW_MS) {
    adxlReadXYZ(x, y, z);
    mag = sqrtf(x * x + y * y + z * z);
    if (fabsf(mag - 1.0f) > STILL_THRESH_G) { moved = true; break; }
    sumX += x; sumY += y; sumZ += z; n++;
    delay(20);
  }
  if (moved || n < 40) { r.rejectedBecause = "recovered"; return r; }

  // Orientation change: dot product of normalised gravity vectors.
  float ax = sumX / n, ay = sumY / n, az = sumZ / n;
  float na = sqrtf(ax * ax + ay * ay + az * az);
  // Reference "upright" orientation captured at the last calm heartbeat.
  static float refX = 0, refY = 0, refZ = 1;
  float dot = (ax * refX + ay * refY + az * refZ) / (na > 0 ? na : 1);
  dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot);
  r.orientationDeg = acosf(dot) * 180.0f / (float)M_PI;

  if (r.orientationDeg < ORIENT_CHANGE_DEG) {
    r.rejectedBecause = "no-orientation-change";
    return r;
  }

  r.isFall = true;
  return r;
}

/* ── location from Wi-Fi access points ──────────────────────── */
String scanLocation() {
  int n = WiFi.scanNetworks(false, false, false, 200);
  String out = "[";
  for (int i = 0; i < n && i < 5; i++) {
    if (i) out += ",";
    out += "{\"bssid\":\"" + WiFi.BSSIDstr(i) + "\",\"rssi\":" + String(WiFi.RSSI(i)) + "}";
  }
  WiFi.scanDelete();
  return out + "]";
}

/* ── alerting ───────────────────────────────────────────────── */
void onMessage(char *topic, byte *payload, unsigned int len) {
  if (len >= 3 && !strncmp((char *)payload, "ACK", 3)) acknowledged = true;
}

bool netUp() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
  if (WiFi.status() != WL_CONNECTED) return false;
  mqtt.setServer(MQTT_HOST, 1883);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(1024);
  if (!mqtt.connect(DEVICE_ID)) return false;
  mqtt.subscribe("care/" DEVICE_ID "/cmd");
  return true;
}

void publishAlert(const char *event, const FallResult &r, uint8_t stage) {
  JsonDocument d;
  d["device"] = DEVICE_ID;
  d["event"]  = event;
  d["stage"]  = stage;
  d["peak_g"] = roundf(r.peakG * 100) / 100.0f;
  d["orient_deg"] = roundf(r.orientationDeg);
  d["impact_ms"]  = r.impactDelayMs;
  d["batt_v"] = roundf(batteryVolts() * 100) / 100.0f;
  d["falls"]  = fallCount;
  d["cancels"]= cancelCount;
  char buf[400];
  size_t n = serializeJson(d, buf, sizeof(buf));
  mqtt.publish("care/" DEVICE_ID "/alert", (uint8_t *)buf, n, true);

  String loc = scanLocation();
  mqtt.publish("care/" DEVICE_ID "/location", loc.c_str(), true);
}

/* Returns true if the wearer cancelled. */
bool cancelWindow() {
  uint32_t start = millis();
  uint8_t intensity = 0;
  while (millis() - start < CANCEL_WINDOW_S * 1000UL) {
    if (digitalRead(PIN_BTN) == LOW) {
      vibe(400);
      tone(PIN_BUZZ, 1200, 300);
      return true;
    }
    // Escalate every 6 s so it becomes progressively harder to ignore.
    uint8_t want = (millis() - start) / 6000;
    if (want != intensity) intensity = want;
    alertPattern(intensity);
    delay(400);
  }
  return false;
}

/* ── sleep ──────────────────────────────────────────────────── */
void sleepNow() {
  adxlRead(0x30);                       // clear latched interrupt
  rtc_gpio_pullup_dis(PIN_INT);
  rtc_gpio_pulldown_en(PIN_INT);
  esp_sleep_enable_ext0_wakeup(PIN_INT, 1);        // wake on HIGH
  esp_sleep_enable_timer_wakeup(HEARTBEAT_S * 1000000ULL);
  Serial.flush();
  esp_deep_sleep_start();
}

/* ── setup runs once per wake ───────────────────────────────── */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_VIBE, OUTPUT); pinMode(PIN_LED, OUTPUT);
  pinMode((int)PIN_BTN, INPUT_PULLUP);
  bootCount++;

  Wire.begin(21, 22);
  adxlBegin();

  esp_sleep_wakeup_cause_t why = esp_sleep_get_wakeup_cause();

  /* Manual SOS: the button bypasses detection entirely. */
  if (digitalRead((int)PIN_BTN) == LOW) {
    FallResult manual = { true, 0, 0, 0, nullptr };
    vibe(600);
    if (netUp()) publishAlert("manual-sos", manual, 1);
    for (int i = 0; i < 20 && !acknowledged; i++) { alertPattern(2); mqtt.loop(); delay(500); }
    sleepNow();
  }

  /* Timer wake: heartbeat only. */
  if (why != ESP_SLEEP_WAKEUP_EXT0) {
    FallResult idle = {};
    if (netUp()) { publishAlert("heartbeat", idle, 0); mqtt.loop(); }
    delay(200);
    sleepNow();
  }

  /* Interrupt wake: run the classifier. */
  digitalWrite(PIN_LED, HIGH);
  FallResult r = classify();
  digitalWrite(PIN_LED, LOW);

  if (!r.isFall) {
    Serial.printf("rejected: %s (peak %.2f g)\n", r.rejectedBecause, r.peakG);
    sleepNow();
  }

  fallCount++;
  Serial.printf("FALL: peak %.2f g, orientation %.0f deg, impact +%u ms\n",
                r.peakG, r.orientationDeg, r.impactDelayMs);

  if (cancelWindow()) {
    cancelCount++;
    if (netUp()) { publishAlert("cancelled", r, 0); mqtt.loop(); }
    sleepNow();
  }

  /* No cancel — escalate. */
  bool online = netUp();
  for (uint8_t stage = 1; stage <= 3 && !acknowledged; stage++) {
    if (online) publishAlert("fall", r, stage);
    uint32_t t0 = millis();
    while (millis() - t0 < 60000UL && !acknowledged) {
      alertPattern(3);
      if (online) mqtt.loop();
      if (digitalRead((int)PIN_BTN) == LOW) { acknowledged = true; break; }
      delay(600);
    }
  }

  if (online) { publishAlert(acknowledged ? "acknowledged" : "unanswered", r, 4); mqtt.loop(); }
  sleepNow();
}

void loop() { /* never reached */ }
adxlWrite(0x28/0x29) free-fall registersThe ADXL345 detects free fall in hardware at about 40 µA. Doing this in firmware would require the ESP32 to stay awake sampling at 100 Hz, which is roughly two thousand times more power and turns months of battery into hours.
Three sequential phases with early returnsEach phase can only reject, never confirm alone. That structure is what gives the low false-positive rate: sitting down passes phase 2 and fails phase 1; dropping the pendant passes 1 and 2 and fails 3.
delay(300) before reading orientationThe impact makes the whole assembly ring mechanically for a couple of hundred milliseconds. Reading the gravity vector during that ringing gives a meaningless orientation.
moved || n < 40 → "recovered"Someone who gets straight back up did not need an alert. Requiring two full seconds of stillness removes a large fraction of the remaining false positives at almost no cost in sensitivity, because a person who has genuinely fallen is rarely up within two seconds.
alertPattern(intensity) escalatingThe cancel prompt gets progressively louder and more insistent over the thirty seconds. A single quiet beep is easy to sleep through; a pattern that escalates is not.
Manual SOS checked before everythingThe button path runs before any classification, so a wearer who feels unwell but has not fallen can still summon help. It is also the simplest possible code path, which is what you want for the function that matters most.
scanLocation() Wi-Fi BSSIDsScanning access points costs about 200 ms and a few tens of milliamps, against seconds and hundreds of milliamps for a GPS fix that will not work indoors anyway. Indoors, known BSSIDs give room-level accuracy, which is exactly what a responder needs.

Configuration & Calibration

Configuration steps

  • Tune FF_THRESH_G and the ADXL345 THRESH_FF register together. Lower catches gentler falls and admits more false wakes; 0.5–0.6 g is the usual compromise.
  • Set IMPACT_THRESH_G for the flooring. Carpet absorbs impact and may need 2.0 g; hard tile produces 4–6 g and tolerates a higher threshold.
  • Set CANCEL_WINDOW_S. Thirty seconds is long enough for a recovered wearer to react and short enough that a real emergency is not delayed.
  • Configure the escalation contacts in your Node-RED or Home Assistant flow, not in firmware. The contact list changes far more often than the firmware does.
  • Capture the reference upright orientation during a calm heartbeat, and re-capture it if the wearer changes how the pendant hangs.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Collect real fall data safely

    Drop the pendant onto a thick mattress from chest height, in several orientations, twenty times. Log peak g, free-fall duration and orientation change for each. That distribution tells you where your thresholds should sit far better than any published figure.

  2. Collect the false-positive set

    Wear it for a normal day, then deliberately sit down heavily twenty times, lie down on a bed twenty times, and set it on a table twenty times. Every one of these must be rejected. If any pass, note which phase failed to reject them and tighten that phase.

  3. Verify sleep current

    Break the battery positive lead and measure in series. You should see 50–90 µA. Above 500 µA means something is still powered — usually a dev board regulator or a permanently connected divider.

  4. Test the escalation end to end

    Trigger a real alert and confirm the notification actually reaches the phone, with the location attached, and that acknowledging it stops the pendant. Do this before the device is relied upon, and repeat quarterly.

Network Architecture & Connectivity

Fall Detection Pendant — network topologyPath taken by telemetry from field node to end user. Edge nodesGatewayCloudClientsPendantbattery, sleepsSecond pendantoptionalWi-Fi, on demandHome routercoverage mattersMQTT 1883Local broker +Node-REDescalation logicFamily phonespush, stagedCare dashboardhistory
Fall Detection Pendant — network topology

Communication protocol

Alerts are published retained so a phone that comes online after the event still sees it. Escalation staging lives in Node-RED rather than firmware, because contact lists and schedules change often and reflashing a pendant on someone's neck is not practical.

The pendant only joins Wi-Fi when it has something to report. Association costs two to four seconds and a few hundred milliamp-milliseconds, which is acceptable a handful of times a day and unaffordable continuously.

Topic / endpointDirectionPayload
care/pendant-01/alertdevice → broker (retained)JSON: event, stage, peak_g, orient_deg, impact_ms, batt_v, falls, cancels
care/pendant-01/locationdevice → broker (retained)JSON array of nearby BSSIDs with RSSI
care/pendant-01/cmdbroker → device"ACK" to stop escalation

Message contract between the device and the broker.

Mobile app integration

A Node-RED flow subscribed to the alert topic drives the escalation: stage 1 notifies the primary contact with an urgent-priority ntfy push, waits 60 seconds for an acknowledgement, then stage 2 adds a second contact, then stage 3 notifies everyone. Each notification carries the peak g, the orientation change and the nearest access points.

Add a missed-heartbeat alarm. If a pendant has not reported in 36 hours it is flat, out of range or broken — and a silent pendant is the most dangerous failure this device has.

Security considerations

  • Use broker authentication. An unauthenticated ACK topic means anyone on the network can silence a real emergency alert.
  • The location data reveals when someone is home and where in the house. Keep it on a local broker, not a cloud service.
  • Make the acknowledgement require a deliberate action in the notification, not a swipe-away — an alert dismissed by accident is worse than one that keeps ringing.

Testing Procedure & Expected Output

Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop — the first failing step tells you exactly where to look.

TestWhat you should see
Measure sleep current50–90 µA. Above 500 µA and the battery target is unreachable.
Drop the pendant onto a mattress from chest heightWake, classify as a fall, and enter the cancel window within about three seconds.
Sit down heavily wearing it, twenty timesZero alerts. Every one should be rejected as no-freefall.
Set it down firmly on a table, twenty timesZero alerts, rejected as recovered or no-orientation-change.
Lie down on a bed slowlyRejected as no-impact — a controlled descent has no impact spike.
Trigger a fall and press cancelAlerts stop immediately, a cancelled event is published, and the pendant returns to sleep.
Trigger a fall and do not cancelEscalation through three stages over about three minutes, with the phone notification arriving at stage 1.
Press the SOS button while idleImmediate alert with no classification, which is the point of the manual path.

Bench-test checklist. If a row fails, stop and fix it before moving on.

Expected output

With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 module. For a battery build, use the bare module rather than a development board — the board's regulator alone would flatten the cell in days. Photograph sourced from Wikimedia Commons — ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

The pendant never wakes on a fall

Likely cause. The interrupt pin is not RTC-capable, the ADXL345 interrupt polarity is wrong, or the latched interrupt was never cleared.

Fix. Use an RTC GPIO (32–39, 25–27, 12–15, 4, 2, 0). Set INT_INVERT off so the interrupt is active high and wake on HIGH. Read register 0x30 before sleeping to clear the latch — an uncleared latch means the pin is already high and the next event produces no edge.

Constant false alarms while walking

Likely cause. Free-fall threshold too high or the time window too short, so ordinary arm swing triggers it.

Fix. Lower THRESH_FF to 0.5 g and raise TIME_FF to 100 ms or more. Arm-swing dips last under 40 ms; a real fall sustains it for 200 ms or longer. The time window is a more effective discriminator than the threshold.

Genuine falls are missed

Likely cause. A fall onto a soft surface or a partly-broken fall never reaches the impact threshold.

Fix. Lower IMPACT_THRESH_G to 2.0 g and verify against your own logged drop data. Accept that a slow slide down a wall may produce neither free fall nor impact — no accelerometer-only device catches that, which is why the manual SOS button is essential rather than optional.

Battery lasts days rather than months

Likely cause. A development board rather than a bare module, or the accelerometer is being polled instead of interrupting.

Fix. A DevKit draws 8–20 mA asleep from its regulator and USB chip. Use a bare ESP32-WROOM with an efficient LDO. Confirm the classifier only runs after an interrupt wake, never on a timer.

Orientation change reads near zero for a real fall

Likely cause. The reference orientation is stale or was captured while the pendant was already lying down.

Fix. Capture the reference during a calm heartbeat when the device has been still and upright for a while, and re-capture whenever the wearer reports the pendant hangs differently.

The alert never reaches the phone

Likely cause. Wi-Fi association is slower than the allowance, or the escalation flow is not running.

Fix. Cache the channel and BSSID in RTC memory to halve association time. Test the whole chain end to end monthly — an escalation flow that silently stopped working is the single most dangerous failure mode this system has.

The sketch will not upload — "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools → Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it — it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 µF electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise — shorten the cable and keep motor wiring away from the USB lead.

Wi-Fi connects but MQTT never does (state -2)

Likely cause. Wrong broker address or port, a firewall in the way, or the broker requiring credentials the sketch is not sending.

Fix. Test from a laptop on the same network first: mosquitto_sub -h <broker> -t "#" -v. If that works, the problem is on the device — check the IP literal, port 1883 (or 8883 for TLS), and that client.setServer() runs before connect(). PubSubClient state codes are documented in its header.

Readings arrive for a while and then stop

Likely cause. The Wi-Fi or MQTT session dropped and the sketch never reconnects, or the broker dropped the client on keep-alive timeout.

Fix. Never assume the link stays up. Check WiFi.status() and client.connected() at the top of every loop and reconnect with exponential backoff. Add a watchdog so a wedged network stack reboots the device instead of going silent.

Performance Optimisation

  • Let the accelerometer do the watching. Its hardware free-fall detection at 40 µA replaces continuous 100 Hz sampling on the MCU, which is the entire reason this runs for months.
  • Cache the Wi-Fi channel and BSSID in RTC memory so an alert connects in about one second instead of four.
  • Scan access points rather than acquiring GPS. A GPS cold fix costs 30 seconds and hundreds of milliamps and does not work indoors, which is where most falls happen.
  • Replace every delay() with a millis() comparison — blocking delays are the single most common cause of dropped readings.
  • Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
  • Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
  • Use uint8_t / uint16_t where the range allows; on an 8-bit AVR a 32-bit add costs four times as much.
  • Batch several samples into one MQTT publish. Radio time, not CPU time, dominates the energy budget.
  • Set the MQTT keep-alive to a value that matches your reporting interval so the broker does not churn reconnections.
  • For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 µA asleep, which is the difference between days and months of runtime.
  • Profile before optimising — print micros() deltas around each stage and fix the slowest one first.

Safety Precautions

  • This is not a medical device and must never be the only safety measure. No accelerometer algorithm catches every fall — a slow slide down a wall produces neither free fall nor impact.
  • The lanyard must break away under load. A fixed cord around the neck of a person who has fallen is a strangulation hazard, and this population is exactly the one at risk.
  • Test the full escalation chain monthly, including that the notification arrives and can be acknowledged. A pendant that alerts into a broken flow is worse than none, because everyone believes it is working.
  • Never disable the manual SOS path to save power or code. It is the function that works when the classifier does not.
  • Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 °C, and never leave a charging pack unattended on a wooden desk.
  • Wear eye protection when soldering or cutting, and solder in a ventilated space — rosin flux fumes are a respiratory irritant.
  • Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
  • Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.

Maintenance

  • Charge when a heartbeat reports below 3.5 V; do not wait for the device to die.
  • Test a real fall and a real escalation quarterly, with the intended recipients.
  • Check the lanyard break-away clasp still releases under load every few months — they stiffen with wear.
  • Re-check every screw terminal and header after the first week — thermal cycling loosens connections that felt tight on day one.
  • Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life — replace it.
  • Keep the broker and dashboard containers patched, and rotate device credentials at least once a year.
  • Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware — not only in flash.
  • Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.

Future Improvements & Upgrades

A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.

  • Add a gyroscope (upgrade to an MPU-6050 or IMU with sensor fusion). Angular velocity during a fall is a strong additional feature and improves discrimination of the slow-slide case.
  • Add a small on-device classifier trained on your own labelled fall and non-fall data. A decision tree with ten features outperforms hand-tuned thresholds and still runs in kilobytes.
  • Add LoRa as a fallback transport so an alert works outside Wi-Fi range, in a garden or a stairwell.
  • Add a speaker and two-way audio so a responder can talk to the wearer immediately, which is what commercial systems do and what most reassures families.
  • Add barometric altitude — a rapid pressure increase of a few pascals is a direct measurement of descending, and it disambiguates a fall from a fall down stairs.
  • Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
  • Add over-the-air firmware updates so you never have to physically reach a deployed node again.
  • Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
  • Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
  • Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
  • Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.

Frequently Asked Questions

Why not just use a single acceleration threshold?

Because it does not work. Sitting down heavily produces 2–3 g, which overlaps completely with a fall onto carpet. Setting a pendant on a table produces more. Any single threshold either misses real falls or fires constantly, and a device that fires constantly gets removed and left in a drawer — which is a worse outcome than no device.

What is the actual accuracy?

With the three-phase approach and thresholds tuned on your own data, published implementations of this class typically report 85–95 % sensitivity with under one false alarm per week. That is good enough to be useful and not good enough to be relied upon alone. The manual SOS button covers the gap.

Why not add a gyroscope?

You should, if you want the best result — angular velocity during a fall is genuinely informative and the MPU-6050 costs the same as the ADXL345. The reason this build uses an accelerometer alone is power: the ADXL345 has hardware free-fall detection at 40 µA, while running a gyroscope continuously costs several milliamps. A hybrid that wakes the gyroscope on the accelerometer interrupt gets both.

How is the location determined without GPS?

By scanning nearby Wi-Fi access points and reporting their BSSIDs and signal strengths. Indoors this gives room-level accuracy once you have mapped which access points are strongest where, and it costs 200 ms instead of the 30 seconds and high current a GPS cold fix needs — and GPS does not work indoors anyway, which is where most falls happen.

What happens if the wearer falls out of Wi-Fi range?

The local alarm still sounds, which is the most important part, but no remote alert is sent. This is the main argument for adding LoRa, which covers a whole property and garden from one gateway. It is also why the pendant should be tested in every room the wearer uses.

Is thirty seconds too long to wait before alerting?

It is a genuine trade-off and worth thinking about. Thirty seconds delays real help slightly; a shorter window means every recoverable stumble becomes a family emergency. The escalating haptic pattern matters more than the duration — a wearer who is conscious will cancel within a few seconds, and one who cannot cancel is exactly the case where the delay costs least relative to the benefit.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. ADXL345 3-axis digital accelerometer — datasheet with free-fall and activity registersAnalog Devices
  2. Bourke et al., "Evaluation of a threshold-based tri-axial accelerometer fall detection algorithm"Gait & Posture, 2007
  3. Noury et al., "Fall detection — Principles and Methods"IEEE EMBS, 2007
  4. Chaudhuri et al., "Fall detection devices and their use with older adults: a systematic review"Journal of Geriatric Physical Therapy
  5. ESP32 deep sleep and ext0 wake sourcesEspressif
  6. WHO — Falls fact sheetWorld Health Organization