Una caída en casa rara vez es grave por el golpe. Lo grave es el tiempo que la persona pasa en el suelo sin poder pedir ayuda: deshidratación, hipotermia y daño muscular aparecen en cuestión de horas, incluso en departamentos donde hay familia entrando y saliendo. Y pedirle a alguien que acaba de caerse que alcance el celular es, justamente, pedirle lo que no puede hacer.
En este tutorial vas a construir un dispositivo vestible del tamaño de un llavero que detecta el impacto, confirma que la persona quedó inmóvil y manda un mensaje de WhatsApp a la familia sin que nadie apriete un botón. Al terminar vas a saber cómo se distingue una caída real de un movimiento brusco, cómo calibrar los umbrales para la persona que lo va a usar, y por qué cada uno de los tres criterios de detección está donde está.
Cómo se decide que alguien se cayó
La parte difícil de un detector de caídas no es detectar el golpe. Es no gritar "¡caída!" cada vez que la persona se sienta fuerte en un sillón o deja el dispositivo sobre la mesa.
Por eso la detección se arma en tres condiciones encadenadas, y las tres tienen que cumplirse en orden:
- Impacto sobre 4,5 g. El MPU-6050 entrega aceleración en los tres ejes. El firmware calcula la magnitud del vector completo,
sqrt(ax² + ay² + az²), no un solo eje. En reposo ese valor es 1 g (la gravedad). Un paso normal llega a 1,5 g, sentarse de golpe ronda los 2 a 3 g, y una caída con impacto contra el suelo supera fácilmente los 4 g. - Cuerpo horizontal sobre 60°. Un golpe por sí solo no es una caída: se te puede caer el dispositivo de la mesa. El firmware calcula el ángulo respecto a la vertical con
acos()sobre el eje principal. Si después del impacto el cuerpo quedó tendido, el ángulo se dispara. - Inmovilidad de 10 segundos. Este es el filtro que realmente salva el proyecto. Si la persona se cayó pero se levanta sola, la alerta se cancela. Solo cuando el giroscopio reporta movimiento angular bajo los 45°/s durante 10 segundos continuos se dispara el aviso.
Recién cuando las tres se cumplen, el ESP32-S3 levanta el WiFi y dispara el mensaje.
Por qué 4,5 g y no otro número
Acá viene la parte que el tutorial original da por resuelta y que en la práctica es lo que más vas a tocar: 4,5 g es un punto de partida, no una constante universal.
El umbral correcto depende del cuerpo y de dónde va montado el sensor. Una persona de 90 kg genera picos bastante mayores que una de 50 kg en la misma caída. Un sensor en la cintura registra menos aceleración que uno colgando del cuello, porque la cintura está más cerca del centro de masa y se mueve menos.
El procedimiento de calibración que conviene hacer antes de confiar en el aparato:
- Carga el firmware y abre el monitor serial a 115200 baudios.
- Pídele a la persona que use el dispositivo una hora haciendo su rutina normal: caminar, sentarse, agacharse, subir una escala.
- Anota el valor máximo de
peakImpactG_Windowque aparezca en ese rato. Ese es tu piso de falsos positivos. - Deja caer el dispositivo (solo el dispositivo, sobre un cojín) desde la altura de la cintura y anota el valor.
- Pon
FALL_G_THRESHOLDa mitad de camino entre ambos números.
Si la rutina normal nunca pasó de 2,8 g y la caída simulada marcó 6,2 g, un umbral de 4,5 g está bien. Si la persona usa bastón y golpea el suelo con fuerza al caminar, quizás su piso sea 3,9 g y tengas que subir el umbral a 5,2 g. Un detector con umbral mal calibrado que avisa tres veces al día termina apagado en un cajón, que es el peor resultado posible.
Ojo con el techo: el firmware configura el MPU-6050 en rango ±8 g. Una caída fuerte contra cerámica puede saturar el sensor, y un valor saturado se lee como exactamente 8 g. Si en tus pruebas ves muchos 8,00 g clavados, sube el rango a MPU6050_RANGE_16_G — pierdes algo de resolución pero dejas de perder eventos.
Hardware y conexiones
El montaje es corto: un microcontrolador, un sensor por I²C, una batería y un divisor de tensión para medir su carga.

El sensor va por I²C en dos pines:
| MPU-6050 | XIAO ESP32-S3 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCL | GPIO 5 (D4) |
| SDA | GPIO 6 (D5) |
La batería LiPo se suelda directamente a los pads B+ y **B, ** que la XIAO trae en la cara inferior. El interruptor SPST va en serie entre el positivo de la batería y B+, para poder apagar el dispositivo sin desarmarlo.
El divisor de tensión: lo que conviene saber antes de soldarlo
Para que el dispositivo reporte cuánta batería le queda hay que medir los 3,7 V de la LiPo con un ADC que solo tolera 3,3 V. La solución del proyecto son dos resistencias de 10 kΩ en serie entre el positivo de la batería y GND, con el punto medio conectado a GPIO 1 (D0). Eso divide la tensión exactamente a la mitad, y el firmware la multiplica por 2 para reconstruirla.
Dos detalles que el original no menciona y que importan en un aparato que funciona con batería:
Ese divisor consume corriente las 24 horas. Con 20 kΩ totales y 4,2 V de batería cargada, son unos 210 µA drenándose permanentemente, incluso con el dispositivo "apagado" si el interruptor no está antes del divisor. En una batería de 800 mAh eso solo son unos 160 días de autodescarga, así que no es crítico. pero es la razón por la que el interruptor debe ir antes de la unión del divisor, no después. Si te importa exprimir la autonomía, sube las resistencias a 100 kΩ cada una y bajas el consumo a 21 µA.
El ADC del ESP32-S3 no es lineal. Por eso el código trae BATTERY_CAL_FACTOR en 1.008f. Ese número no es mágico ni se copia: se calcula midiendo la batería con un multímetro y dividiendo ese valor real por el que imprime el monitor serial. Cada placa da un factor levemente distinto.
La carcasa impresa

La carcasa son dos piezas diseñadas en Fusion 360, de 60 mm de largo × 55 mm de ancho × 25 mm de alto, con calados para el puerto USB-C, el interruptor SPST y la salida de la antena. Se imprime con 0,2 mm de altura de capa y 20 % de relleno en cualquier laminador estándar (Cura, PrusaSlicer, Orca).

Un punto de montaje que decide si el aparato funciona: el sensor tiene que ir plano contra el cuerpo, con el eje X apuntando verticalmente a lo largo de la columna y el eje Z hacia afuera de la cintura. El cálculo del ángulo usa el eje X como referencia de "vertical", así que si montas el sensor girado 90°, el dispositivo va a creer que la persona está acostada todo el tiempo.
Configuración de la nube y del aviso por WhatsApp
El firmware publica seis variables a un panel en CircuitDigest Cloud y usa su pasarela para enviar el WhatsApp. El registro es gratuito.

Entra a Dashboard, abre la sección Device en la barra lateral y usa Add New Device. El nombre es libre; en el proyecto original quedó como "Elderly Fall Detection".

Con el dispositivo creado, abre Variables, selecciónalo en el desplegable y usa Add Variable. Cada variable necesita un nombre visible y una clave (key); la dirección se deja en bidireccional.

Son seis variables y el orden de las claves importa, porque el firmware las tiene fijas en sus #define:
| Clave | Qué transporta |
|---|---|
analog-input-1 |
Fuerza de impacto (g) |
analog-input-2 |
Ángulo de orientación (grados) |
analog-input-3 |
Duración de la inmovilidad (segundos) |
analog-input-4 |
Nivel de batería (%) |
analog-input-5 |
LED de alerta de caída |
analog-input-6 |
Botón de reinicio (nube → dispositivo) |

Después vas a Dashboard, seleccionas el dispositivo y agregas un widget Value Display por cada variable, apuntando cada uno a su clave.

Por último, en la pestaña Home, abre la sección de notificaciones de WhatsApp, vincula tu número y verifica el código que llega por mensaje. Ahí termina el lado de la nube.

Anota el Device ID, la Connection Key y la API Key: van en el firmware.
El firmware, por partes
El código está escrito para el Arduino IDE. Antes de compilar necesitas las librerías CircuitDigestCloud, Adafruit_MPU6050 y Adafruit_Sensor.
Credenciales y umbrales
#include <CircuitDigestCloud.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
// ── Credentials ─────────────────────────────────────────────────────────
#define WIFI_SSID "xxxxxxxxxxxx" //Your Wi-Fi name
#define WIFI_PASS "xxxxxxxxxxxx" //Your Wi-Fi password
#define DEVICE_ID "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"//Your Device ID
#define CONNECTION_KEY "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"//Your Connection Key
#define API_KEY "xxxxxxxxxxxxxxxxxxx"//Your API key
#define PHONE_NUMBER "xxxxxxxxxxxx"//Your mobile phone number
// ── Dashboard Key Mapping ───────────────────────────────────────────────
#define KEY_IMPACT_FORCE "analog-input-1"
#define KEY_ORIENTATION "analog-input-2"
#define KEY_IMMOBILITY "analog-input-3"
#define KEY_BATTERY "analog-input-4"
#define KEY_FALL_LED "analog-input-5"
#define KEY_RESET_BTN "analog-input-6"
// ── XIAO ESP32-S3 Hardware Pinouts ─────────────────────────────────────
#define XIAO_SDA_PIN 5 // D4 / GPIO 5
#define XIAO_SCL_PIN 6 // D5 / GPIO 6
#define BATTERY_ADC_PIN 1 // D0 / GPIO 1 (A0)
// ── Battery Calibration ─────────────────────────────────────────────────
// factor = true_voltage_from_multimeter / reconstructed_voltage_from_serial
// RECALIBRATE per-board using the steps in the header comment above.
#define BATTERY_CAL_FACTOR 1.008f
// ── Detection Thresholds ────────────────────────────────────────────────
#define FALL_G_THRESHOLD 4.5f // Impact threshold (g)
#define FALL_ANGLE_LIMIT 60.0f // Horizontal orientation threshold (degrees)
#define IMMOBILITY_GYRO_MAX 45.0f // Deg/s noise limit to qualify as stationary
const char* host = "www.circuitdigest.cloud";
Reemplaza las credenciales WiFi y las tres claves de CircuitDigest por las tuyas. Recuerda que el ESP32-S3 solo se conecta a redes de 2,4 GHz: si tu router publica una sola red mixta 2,4/5 GHz, separa las bandas o crea una red de invitados en 2,4 GHz.
Medición de batería
// ── Battery Calculation for XIAO ESP32-S3 ──────────────────────────────
float readBatteryPercentage() {
// Force 11dB attenuation on GPIO 1 to allow reading full 0-3.1V range
analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
uint32_t sumMv = 0;
for (int i = 0; i < 20; i++) {
sumMv += analogReadMilliVolts(BATTERY_ADC_PIN);
delay(1);
}
float pinMv = sumMv / 20.0f;
// Reconstruct battery voltage (10k/10k divider = pin voltage * 2)
float batteryVolts = (pinMv * 2.0f) / 1000.0f;
// Apply per-board calibration to correct for residual ADC offset
// (divider ratio itself already confirmed accurate on this board).
batteryVolts *= BATTERY_CAL_FACTOR;
Serial.print("[BATTERY DEBUG] Pin mV: ");
Serial.print(pinMv);
Serial.print(" | Reconstructed Battery Volts (calibrated): ");
Serial.println(batteryVolts, 2);
float pct = 0.0f;
if (batteryVolts >= 4.15f) pct = 100.0f;
else if (batteryVolts >= 3.95f) pct = 80.0f + ((batteryVolts - 3.95f) / 0.20f) * 20.0f;
else if (batteryVolts >= 3.75f) pct = 40.0f + ((batteryVolts - 3.75f) / 0.20f) * 40.0f;
else if (batteryVolts >= 3.50f) pct = 10.0f + ((batteryVolts - 3.50f) / 0.25f) * 30.0f;
else if (batteryVolts >= 3.20f) pct = 0.0f + ((batteryVolts - 3.20f) / 0.30f) * 10.0f;
else pct = 0.0f;
return pct;
}
Fíjate en el tramo a tramo del final: no es una regla de tres. La curva de descarga de una LiPo es plana en el medio y cae en picada al final, así que un mapeo lineal de 3,2 a 4,2 V a 0 a 100 % mostraría "60 %" durante días y después se apagaría de golpe. Los cinco tramos aproximan esa curva real.
Envío del WhatsApp
// ── WhatsApp Emergency Alert ─────────────────────────────────────────────
void sendWhatsAppFallAlert(float impactG, float angle, float immobilitySec) {
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(5000);
Serial.println("Connecting to CircuitDigest Cloud WhatsApp Gateway...");
if (!client.connect(host, 443)) {
Serial.println("WhatsApp API Connection FAILED");
return;
}
String payload =
"{\"phone_number\":\"" + String(PHONE_NUMBER) + "\","
"\"template_id\":\"threshold_violation_alert\","
"\"variables\":{"
"\"device_name\":\"XIAO ESP32-S3 Fall Node\","
"\"parameter\":\"Fall Event\","
"\"measured_value\":\"" + String(impactG, 2) + "g Impact / " + String(angle, 1) + " deg\","
"\"limit\":\"Immobile: " + String((int)immobilitySec) + " sec\","
"\"location\":\"Personal Wearable\"}}";
client.println("POST /api/v1/whatsapp/send HTTP/1.1");
client.println("Host: www.circuitdigest.cloud");
client.println("X-API-Key: " + String(API_KEY));
client.println("Content-Type: application/json");
client.println("Connection: close");
client.print("Content-Length: ");
client.println(payload.length());
client.println();
client.print(payload);
unsigned long responseStart = millis();
while (client.connected() && !client.available()) {
if (millis() - responseStart > 5000) {
Serial.println("WhatsApp response TIMEOUT");
client.stop();
return;
}
delay(10);
}
Serial.println("WhatsApp Alert Request Dispatched Successfully.");
client.stop();
}
El mensaje viaja como JSON sobre HTTPS con el pico de fuerza g, el ángulo y los segundos de inmovilidad. Nota que el código usa client.setInsecure(), o sea que no valida el certificado del servidor: está bien para una demo, pero si vas a dejar esto funcionando en serio conviene cargar el certificado raíz con client.setCACert().
Arranque
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(BATTERY_ADC_PIN, INPUT);
analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
Wire.begin(XIAO_SDA_PIN, XIAO_SCL_PIN);
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip on XIAO I2C bus!");
while (1) delay(10);
}
mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_260_HZ);
CDcloud.subscribe(KEY_RESET_BTN, onResetButton);
Serial.println("Connecting XIAO ESP32-S3 to Wi-Fi & CircuitDigest Cloud...");
if (!CDcloud.begin(WIFI_SSID, WIFI_PASS, DEVICE_ID, CONNECTION_KEY, API_KEY)) {
Serial.println("CDcloud begin() failed — Rebooting...");
delay(2000);
ESP.restart();
}
for (int i = 0; i < 10; i++) {
CDcloud.loop();
delay(100);
}
float initBattery = readBatteryPercentage();
Serial.println("[INIT PUBLISH] Sending initial dashboard state...");
CDcloud.publish({{KEY_IMPACT_FORCE, 1.0f},
{KEY_ORIENTATION, 0.0f},
{KEY_IMMOBILITY, 0.0f},
{KEY_BATTERY, initBattery},
{KEY_FALL_LED, 0.0f}});
CDcloud.publish({{KEY_RESET_BTN, 0.0f}});
Serial.println("System Ready. Fall detection active on XIAO ESP32-S3.");
}
Acá se configura el I²C en los pines 5 y 6, se pone el acelerómetro en ±8 g y el giroscopio en ±500 °/s, y se registra el callback del botón de reinicio remoto: eso es lo que le permite a la familia cancelar una falsa alarma desde el panel web sin tocar el dispositivo.
Detección del impacto y la postura
float instantG = sqrt(ax_g * ax_g + ay_g * ay_g + az_g * az_g);
if (instantG > peakImpactG_Window) {
peakImpactG_Window = instantG;
}
if (instantG > 0.05f) {
float ratio = fabs(ax_g) / instantG;
if (ratio > 1.0f) ratio = 1.0f;
filteredAngle = acos(ratio) * (180.0f / M_PI);
}
if (peakImpactG_Window >= FALL_G_THRESHOLD && filteredAngle >= FALL_ANGLE_LIMIT) {
if (!isFallDetected) {
isFallDetected = true;
fallStartMs = millis();
alertSent = false;
Serial.println(">>> IMPACT & HORIZONTAL POSTURE DETECTED! <<<");
}
}
El if (ratio > 1.0f) ratio = 1.0f; parece un detalle menor y no lo es: acos() devuelve NaN con argumentos mayores que 1, y el ruido del acelerómetro puede hacer que fabs(ax_g) supere apenas a instantG por redondeo. Sin esa línea, el ángulo se contamina con NaN y todas las comparaciones posteriores fallan en silencio.
Confirmación de inmovilidad
float totalGyroMotion = (fabs(g.gyro.x) + fabs(g.gyro.y) + fabs(g.gyro.z)) * (180.0f / M_PI);
if (isFallDetected) {
if (totalGyroMotion < IMMOBILITY_GYRO_MAX) {
currentImmobility = (millis() - fallStartMs) / 1000.0f;
} else {
isFallDetected = false;
currentImmobility = 0.0f;
alertSent = false;
CDcloud.publish({{KEY_FALL_LED, 0.0f}});
Serial.println("Motion detected post-fall — Alert auto-cleared.");
}
if (currentImmobility >= 10.0f && !alertSent) {
sendWhatsAppFallAlert(peakImpactG_Window, filteredAngle, currentImmobility);
alertSent = true;
}
} else {
currentImmobility = 0.0f;
}
La librería del MPU-6050 entrega el giroscopio en radianes por segundo, por eso la conversión a grados. Se suman los valores absolutos de los tres ejes: no interesa hacia dónde gira, solo cuánto se mueve en total.
Pruebas

La verificación del proyecto original se hizo dejando caer el dispositivo sobre una colchoneta simulando tropiezos hacia adelante, resbalones hacia atrás y caídas laterales, para comprobar que dispara en distintas direcciones. La secuencia mínima que tienes que reproducir: un golpe seco que cruce los 4,5 g, dejar el sensor plano con más de 60° de inclinación, y no moverlo durante 10 segundos.
Además de eso, haz la prueba que más importa y que casi nadie hace: usa el dispositivo un día completo sin simular ninguna caída y cuenta cuántas alertas falsas llegaron. Si llegó aunque sea una, vuelve a la sección de calibración antes de entregárselo a alguien.
Problemas frecuentes
| Síntoma | Qué revisar |
|---|---|
Failed to find MPU6050 chip |
Cableado de SDA/SCL invertido, o el sensor alimentado con 5 V en lugar de 3,3 V. Las resistencias de pull up del bus I²C ya vienen en la mayoría de los módulos MPU-6050. |
| No conecta al WiFi | SSID o contraseña mal escritos, o red de 5 GHz. El ESP32-S3 no soporta 5 GHz. |
| El ángulo marca 90° con la persona de pie | El sensor está montado girado. El eje X debe quedar vertical, a lo largo de la columna. |
| El porcentaje de batería no coincide con el multímetro | Recalcula BATTERY_CAL_FACTOR dividiendo la tensión medida con multímetro por la que imprime el monitor serial. |
| Alertas falsas al sentarse | Sube FALL_G_THRESHOLD siguiendo el procedimiento de calibración. |
Variantes y mejoras
Avisa aunque se caiga internet. El punto débil del diseño es que todo depende del WiFi de la casa y de la nube. Un corte de luz deja el dispositivo mudo justo cuando la persona podría tropezar en la oscuridad. Agrega un zumbador activo de 5 V en un GPIO libre y hazlo sonar en cuanto se confirme la caída, antes de intentar el envío: así alguien en la casa escucha aunque el mensaje nunca salga. Son dos cables y cinco líneas de código.
Ventana para cancelar la alarma desde el propio dispositivo. Hoy la única forma de cancelar una falsa alarma es desde el panel web. Un pulsador en la carcasa que, presionado durante esos 10 segundos de espera, ponga isFallDetected = false, le devuelve el control a la persona que lo usa. Psicológicamente cambia mucho un aparato que puedes callar tú mismo.
Ubicación aproximada sin GPS. Si el proyecto se usa en una casa de dos pisos, saber dónde ocurrió la caída ahorra minutos. El ESP32-S3 puede escanear las redes WiFi visibles y reportar la intensidad de señal de cada una; con dos o tres routers o repetidores en la casa, la combinación de RSSI distingue bastante bien entre pisos o alas. Se agrega al mismo JSON que ya se envía, sin hardware extra.
Registro histórico en microSD. Guardar la magnitud g y el ángulo cada segundo en una tarjeta microSD por SPI te deja revisar después qué pasó exactamente, y es la única manera seria de afinar los umbrales con datos reales en vez de a ojo.
Personalización para Chile
Todos los componentes de este proyecto son de uso corriente y los encuentras en el catálogo de MechatronicStore. Estos son los equivalentes locales de lo que usa el tutorial original:
- Placa XIAO ESP32-S3. es el núcleo del proyecto y no conviene reemplazarlo: el tamaño y los pads B+/B, para la batería LiPo son justamente lo que hace posible el formato vestible. Un ESP32 DevKit común funciona eléctricamente, pero mide el triple y no tiene circuito de carga integrado.
- Módulo MPU-6050. acelerómetro y giroscopio de 6 ejes. Es el mismo integrado que usa el tutorial, sin equivalencias necesarias.
- Batería LiPo 3.7V 800mAh. cualquier celda LiPo de una sola celda entre 500 y 1000 mAh sirve; la de 800 mAh es la que calza con las medidas de la carcasa impresa.
- Resistencia 10kΩ. necesitas dos, para el divisor de tensión.
- Interruptor SPST. el de tamaño mini es el que calza en el calado de la carcasa.
- Cable USB-C. para programar la placa y cargar la batería.
- Antena 2.4GHz. la XIAO ESP32-S3 trae conector u.FL y su antena externa incluida; si la tuya vino sin ella, es un repuesto barato.
Revisa el precio y la disponibilidad actual de cada componente directamente en el catálogo antes de armar tu lista, porque el stock rota. La carcasa impresa en 3D la produces tú con los archivos STL del repositorio; no es un componente de compra.
Recursos
- Tutorial original: Elderly Fall Detection System Using ESP32. Circuit Digest
- Repositorio GitHub: Circuit Digest/Elderly Fall Detection System
- Código y archivos del proyecto (.zip): descarga directa
- Librería del sensor: Adafruit MPU6050
Versión chilena inspirada en el proyecto de Circuit Digest, con componentes en stock local en MechatronicStore y una sección de calibración de umbrales que el original no incluye.




