Main sketch (wheelchair_fall_alert.ino)
arduino · 237 lines
Copy
// TARGET BOARD: ESP32 (ESP32U Development Board)
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
// ----------------------------
// Pin constants (easy to change)
// ----------------------------
// I2C uses default ESP32 pins: SDA=GPIO21, SCL=GPIO22 (shared by OLED + MPU6050)
static const int PIN_BUZZER = 25; // Active buzzer module input (ON/OFF). Use a safe GPIO output pin.
static const int PIN_SNOOZE = 27; // Momentary button to GND (uses internal pull-up)
// ----------------------------
// OLED configuration
// ----------------------------
static const int OLED_WIDTH = 128;
static const int OLED_HEIGHT = 64;
static const int OLED_RESET = -1; // Most 0.96" I2C OLEDs do not use a reset pin
static const uint8_t OLED_ADDR = 0x3C;
// ----------------------------
// MPU6050 configuration
// ----------------------------
static const uint8_t MPU_ADDR = 0x68; // Typical for GY-521 (AD0 low)
// ----------------------------
// WiFi + Telegram configuration (EDIT THESE)
// ----------------------------
static const char* WIFI_SSID = "YOUR_WIFI_SSID";
static const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
// Telegram bot token from @BotFather, looks like: 123456789:ABCdef...
static const char* TELEGRAM_BOT_TOKEN = "YOUR_BOT_TOKEN_HERE";
// Chat ID to receive alerts (your user ID or a group ID). Example: "123456789" or "-1001234567890"
static const char* TELEGRAM_CHAT_ID = "YOUR_CHAT_ID_HERE";
// ----------------------------
// Behavior tuning (thresholds)
// ----------------------------
// Impact detection: acceleration magnitude spike (m/s^2). 1g ~ 9.81 m/s^2.
// Typical "hard bump" might exceed ~2.5g to 3.5g depending on mounting.
static const float IMPACT_G_THRESHOLD_G = 2.8f; // in g
// Free-fall detection: acceleration magnitude near 0g.
static const float FREEFALL_G_THRESHOLD_G = 0.35f; // in g
// Tilt detection: sustained tilt angle (degrees) after event.
static const float TILT_ANGLE_DEG_THRESHOLD = 55.0f; // degrees from "upright"
// How long tilt must be sustained to confirm (ms)
static const uint32_t TILT_SUSTAIN_MS = 2500;
// Countdown duration before sending Telegram alert (seconds)
static const uint32_t ALERT_COUNTDOWN_S = 20;
// Heartbeat interval (ms): daily "device OK"
static const uint32_t HEARTBEAT_INTERVAL_MS = 24UL * 60UL * 60UL * 1000UL;
// Sensor sampling interval (ms)
static const uint32_t SAMPLE_INTERVAL_MS = 50;
// Debounce for button (ms)
static const uint32_t BUTTON_DEBOUNCE_MS = 35;
// ----------------------------
// Globals
// ----------------------------
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
Adafruit_MPU6050 mpu;
WiFiClientSecure tlsClient;
// State machine for alert flow
enum AlertState {
STATE_IDLE = 0,
STATE_SUSPECTED_EVENT, // impact/free-fall detected; now checking sustained tilt
STATE_COUNTDOWN, // confirmed; buzzer + countdown; snooze cancels
STATE_SENT, // alert sent; wait for reset conditions
STATE_ERROR // sensor/display init error
};
AlertState state = STATE_IDLE;
uint32_t lastSampleMs = 0;
uint32_t lastHeartbeatMs = 0;
uint32_t eventStartMs = 0; // when impact/free-fall detected
uint32_t tiltStartMs = 0; // when tilt first exceeded threshold
uint32_t countdownStartMs = 0; // when countdown started
bool oledOK = false;
bool mpuOK = false;
// Button handling
bool lastButtonRaw = true; // pull-up => HIGH when not pressed
bool buttonStable = true;
uint32_t lastButtonChangeMs = 0;
// For display updates
uint32_t lastDisplayMs = 0;
// ----------------------------
// Helper: safe buzzer control (active buzzer: ON/OFF)
// ----------------------------
void buzzerOn() {
digitalWrite(PIN_BUZZER, HIGH);
}
void buzzerOff() {
digitalWrite(PIN_BUZZER, LOW);
}
// ----------------------------
// Helper: read snooze button with debounce (returns true when pressed event occurs)
// Wiring: one side to GPIO, other side to GND. Internal pull-up enabled.
// ----------------------------
bool snoozePressedEvent() {
bool raw = digitalRead(PIN_SNOOZE); // HIGH = not pressed, LOW = pressed
if (raw != lastButtonRaw) {
lastButtonRaw = raw;
lastButtonChangeMs = millis();
}
// If stable long enough, accept new stable state
if ((millis() - lastButtonChangeMs) > BUTTON_DEBOUNCE_MS) {
if (buttonStable != raw) {
buttonStable = raw;
// Detect transition to pressed (LOW)
if (buttonStable == LOW) {
return true;
}
}
}
return false;
}
// ----------------------------
// Helper: OLED text rendering
// ----------------------------
void oledClearAndPrint(const String& line1, const String& line2 = "", const String& line3 = "", const String& line4 = "") {
if (!oledOK) return;
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(line1);
if (line2.length()) display.println(line2);
if (line3.length()) display.println(line3);
if (line4.length()) display.println(line4);
display.display();
}
// ----------------------------
// Helper: connect WiFi (non-blocking-ish with timeout)
// ----------------------------
bool ensureWiFiConnected(uint32_t timeoutMs) {
if (WiFi.status() == WL_CONNECTED) return true;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
delay(50);
}
return (WiFi.status() == WL_CONNECTED);
}
// ----------------------------
// Helper: URL encode minimal set (spaces/newlines mainly)
// ----------------------------
String urlEncode(const String& s) {
String out;
out.reserve(s.length() * 3);
const char* hex = "0123456789ABCDEF";
for (size_t i = 0; i < s.length(); i++) {
uint8_t c = (uint8_t)s[i];
// Unreserved characters per RFC3986
bool unreserved =
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~';
if (unreserved) {
out += (char)c;
} else {
out += '%';
out += hex[(c >> 4) & 0x0F];
out += hex[c & 0x0F];
}
}
return out;
}
// ----------------------------
// Helper: send Telegram message via HTTPS (no extra libraries)
// Uses api.telegram.org with a simple GET request.
// Note: This uses setInsecure() to avoid certificate management in a kit.
// ----------------------------
bool telegramSendMessage(const String& text) {
if (!ensureWiFiConnected(8000)) {
Serial.println("WiFi not connected; cannot send Telegram message.");
return false;
}
tlsClient.setInsecure(); // Simplifies TLS for kits; acceptable for DIY but not best practice.
const char* host = "api.telegram.org";
const int httpsPort = 443;
if (!tlsClient.connect(host, httpsPort)) {
Serial.println("TLS connect to Telegram failed.");
return false;
}
String url = "/bot";
url += TELEGRAM_BOT_TOKEN;
url += "/sendMessage?chat_id=";
url += TELEGRAM_CHAT_ID;
url += "&text=";
url += urlEncode(text);
// Basic HTTP/1.1 GET
tlsClient.print(String("GET ") + url + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"User-Agent: ESP32-FallAlertKit\r\n" +
"Connection: close\r\n\r\n");
Install: WiFi, WiFiClientSecure, Wire, Adafruit GFX Library, Adafruit SSD1306, Adafruit MPU6050, Adafruit Unified Sensor — Set your WiFi SSID/PASSWORD, Telegram BOT token and CHAT_ID, and confirm the buzzer/button GPIO pins match your wiring before uploading.