Knee Rehabilitation Device - BSE Summer
This project is a wearable knee rehabilitation device that helps people perform squats with safe, correct form during recovery. Built around an ESP32 microcontroller, it uses a flex sensor to track how deep the knee bends and an accelerometer + gyroscope to detect when the knee caves inward, sounding a buzzer in real time so the user can correct themselves instantly. The biggest challenge was translating messy, noisy sensor data into reliable feedback: it took lots of testing and calibration to figure out which sensor readings actually corresponded to a good squat versus a bad one, but landing on a system that counts reps, tracks range of motion, and catches bad form the moment it happens made all the debugging worth it.
| Engineer | School | Area of Interest | Grade |
|---|---|---|---|
| Ritwik C | Evergreen Valley High School | Computer Science | Incoming Junior |

#include <Adafruit_LSM6DS3TRC.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
Adafruit_LSM6DS3TRC lsm6ds;
// ---------- BLE (Nordic UART Service) ----------
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) { deviceConnected = true; }
void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
pServer->startAdvertising();
}
};
void sendBoth(String msg) {
Serial.print(msg);
if (deviceConnected) {
pTxCharacteristic->setValue(msg.c_str());
pTxCharacteristic->notify();
}
}
// ---------- Pins ----------
#define FLEX_PIN 32
#define BUZZER_PIN 13
#define RED_LED 25
#define GREEN_LED 26
#define BUTTON_PIN 27
// ---- LEVEL SYSTEM ----
// Higher level = more bend allowed before it warns.
const int LEVEL_THRESHOLDS[3] = { 210, 300, 400 }; // level 1, 2, 3
int currentLevel = 0; // 0-indexed: 0=L1, 1=L2, 2=L3
// ---- Flex over-bend ----
const int OVERBEND_CONFIRM_COUNT = 2;
const int FLEX_SAMPLES = 50;
// ---- Squat detection (X axis) ----
const float SQUAT_X_THRESHOLD = 9;
// ---- Cave detection (Z axis) ----
const float CAVE_Z_THRESHOLD = 6;
// ---- Consecutive-reading confirmation ----
const int CAVE_CONFIRM_COUNT = 2;
const int SAMPLES = 5;
int caveStreak = 0;
int overBendStreak = 0;
bool lastButtonState = HIGH; // for button edge detection
unsigned long lastBLEStatus = 0;
const unsigned long BLE_STATUS_INTERVAL = 1000;
void setLEDs(bool green, bool red) {
digitalWrite(GREEN_LED, green ? HIGH : LOW);
digitalWrite(RED_LED, red ? HIGH : LOW);
}
// Beep N times to announce the level (1 beep = L1, 2 = L2, 3 = L3)
void announceLevel(int level) {
for (int i = 0; i <= level; i++) {
digitalWrite(BUZZER_PIN, HIGH);
setLEDs(false, true);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
setLEDs(true, false);
delay(150);
}
}
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
setLEDs(true, false);
Wire.begin(21, 22);
if (!lsm6ds.begin_I2C()) { Serial.println("Sensor not found!"); while (1) delay(10); }
BLEDevice::init("KneeRehab");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
pTxCharacteristic = pService->createCharacteristic(
CHAR_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY);
pService->createCharacteristic(CHAR_UUID_RX, BLECharacteristic::PROPERTY_WRITE);
pTxCharacteristic->addDescriptor(new BLE2902());
pService->start();
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("BLE ready - connect to 'KneeRehab'");
Serial.print("Starting at LEVEL 1 (limit ");
Serial.print(LEVEL_THRESHOLDS[0]);
Serial.println(") - press button to change level");
}
void beepInward() {
setLEDs(false, true);
for (int i = 0; i < 2; i++) {
digitalWrite(BUZZER_PIN, HIGH); delay(120);
digitalWrite(BUZZER_PIN, LOW); delay(120);
}
}
void buzzOverBend() {
setLEDs(false, true);
digitalWrite(BUZZER_PIN, HIGH);
delay(600);
digitalWrite(BUZZER_PIN, LOW);
}
void loop() {
// --- Button: cycle level 1 -> 2 -> 3 -> 1 ---
bool buttonNow = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH && buttonNow == LOW) { // just pressed
currentLevel = (currentLevel + 1) % 3;
sendBoth(">>> LEVEL " + String(currentLevel + 1)
+ " (limit " + String(LEVEL_THRESHOLDS[currentLevel]) + ")\n");
announceLevel(currentLevel);
delay(50); // debounce
}
lastButtonState = buttonNow;
// The active threshold for this level
int flexLimit = LEVEL_THRESHOLDS[currentLevel];
// --- Flex sensor: heavy averaging ---
long flexSum = 0;
for (int i = 0; i < FLEX_SAMPLES; i++) {
flexSum += analogRead(FLEX_PIN);
delay(1);
}
int flexRaw = flexSum / FLEX_SAMPLES;
// --- Accelerometer: averaged X, Y, Z ---
float xAvg = 0, zAvg = 0, yAvg = 0;
for (int i = 0; i < SAMPLES; i++) {
sensors_event_t a, g, t;
lsm6ds.getEvent(&a, &g, &t);
xAvg += a.acceleration.x;
zAvg += a.acceleration.z;
yAvg += a.acceleration.y;
delay(5);
}
xAvg /= SAMPLES;
zAvg /= SAMPLES;
yAvg /= SAMPLES;
bool inSquat = (xAvg < SQUAT_X_THRESHOLD);
// --- STANDING OVERRIDE ---
if (!inSquat) {
caveStreak = 0;
}
bool caving = inSquat && (zAvg < CAVE_Z_THRESHOLD);
if (caving) caveStreak++; else caveStreak = 0;
// --- Over-bend uses the CURRENT LEVEL's threshold ---
bool overBent = (flexRaw > 0) && (flexRaw < 4095) && (flexRaw > flexLimit);
if (overBent) overBendStreak++; else overBendStreak = 0;
// --- LED state: green by default, red on any fault ---
bool fault = caving || overBent;
setLEDs(!fault, fault);
// --- Build status line ---
String status = "L" + String(currentLevel + 1)
+ " (lim " + String(flexLimit) + ")"
+ " | FlexRaw: " + String(flexRaw)
+ " | X: " + String(xAvg, 2)
+ " | Y: " + String(yAvg, 2)
+ " | Z: " + String(zAvg, 2);
if (!inSquat) status += " -> STANDING";
else if (caving) status += " -> SQUATTING WHILE CAVING!";
else status += " -> SQUAT (good)";
Serial.println(status);
if (millis() - lastBLEStatus > BLE_STATUS_INTERVAL) {
if (deviceConnected) {
pTxCharacteristic->setValue((status + "\n").c_str());
pTxCharacteristic->notify();
}
lastBLEStatus = millis();
}
// --- Fault 1: knee caving inward ---
if (caveStreak >= CAVE_CONFIRM_COUNT) {
sendBoth(">>> KNEE CAVING INWARD!\n");
beepInward();
caveStreak = 0;
}
// --- Fault 2: bent too far (level-dependent) ---
if (overBendStreak >= OVERBEND_CONFIRM_COUNT) {
sendBoth(">>> OVER-BEND! Come back up.\n");
buzzOverBend();
overBendStreak = 0;
}
delay(20);
}
#include <Adafruit_LSM6DS3TRC.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
Adafruit_LSM6DS3TRC lsm6ds;
// ---------- BLE (Nordic UART Service) ----------
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHAR_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
class MyServerCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer *pServer) { deviceConnected = true; }
void onDisconnect(BLEServer *pServer) {
deviceConnected = false;
pServer->startAdvertising(); // allow reconnect
}
};
// Send a line to both Serial and BLE
void sendBoth(String msg) {
Serial.print(msg);
if (deviceConnected) {
pTxCharacteristic->setValue(msg.c_str());
pTxCharacteristic->notify();
}
}
// ---------- Pins ----------
#define FLEX_PIN 32 // analog input from flex sensor divider
#define BUZZER_PIN 13 // active buzzer
// ---- Squat detection (X axis) ----
const float SQUAT_X_THRESHOLD = 9.0; // X below this = squat, above = standing
// ---- Cave detection (Z axis) ----
const float CAVE_Z_THRESHOLD = -2.0; // while squatting, Z below this = caving
// ---- Flex over-bend (raw values) ----
const int FLEX_UPPER_LIMIT = 1900; // raw reading = bent too far
const int FLEX_LOWER_LIMIT = 320; // raw reading = bent too far (other direction)
// ---- Consecutive-reading confirmation ----
const int CAVE_CONFIRM_COUNT = 2; // caving reads in a row needed to buzz
const int SAMPLES = 5; // light averaging
int caveStreak = 0; // counts consecutive caving readings
// ---- BLE status streaming rate limit ----
unsigned long lastBLEStatus = 0;
const unsigned long BLE_STATUS_INTERVAL = 1000; // send status once per second
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
pinMode(BUZZER_PIN, OUTPUT);
Wire.begin(21, 22);
if (!lsm6ds.begin_I2C()) { Serial.println("Sensor not found!"); while (1) delay(10); }
// --- BLE setup ---
BLEDevice::init("KneeRehab");
BLEServer *pServer = BLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
BLEService *pService = pServer->createService(SERVICE_UUID);
pTxCharacteristic = pService->createCharacteristic(
CHAR_UUID_TX,
BLECharacteristic::PROPERTY_NOTIFY
);
pService->createCharacteristic(
CHAR_UUID_RX,
BLECharacteristic::PROPERTY_WRITE
);
pTxCharacteristic->addDescriptor(new BLE2902());
pService->start();
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
pServer->getAdvertising()->start();
Serial.println("BLE ready - connect to 'KneeRehab'");
Serial.println("Ready - start squatting!");
}
// Two short beeps = knee caving inward
void beepInward() {
for (int i = 0; i < 2; i++) {
digitalWrite(BUZZER_PIN, HIGH); delay(120);
digitalWrite(BUZZER_PIN, LOW); delay(120);
}
}
// Long continuous buzz = bent too far
void buzzOverBend() {
digitalWrite(BUZZER_PIN, HIGH);
delay(600);
digitalWrite(BUZZER_PIN, LOW);
}
void loop() {
// --- Flex sensor: raw reading ---
int flexRaw = analogRead(FLEX_PIN);
// --- Accelerometer: averaged X and Z ---
float xAvg = 0, zAvg = 0;
for (int i = 0; i < SAMPLES; i++) {
sensors_event_t a, g, t;
lsm6ds.getEvent(&a, &g, &t);
xAvg += a.acceleration.x;
zAvg += a.acceleration.z;
delay(5);
}
xAvg /= SAMPLES;
zAvg /= SAMPLES;
bool inSquat = (xAvg < SQUAT_X_THRESHOLD);
bool caving = inSquat && (zAvg < CAVE_Z_THRESHOLD);
// --- Track consecutive caving readings ---
if (caving) {
caveStreak++;
} else {
caveStreak = 0;
}
// --- Build the status line ---
String status = "Flex: " + String(flexRaw)
+ " | X: " + String(xAvg, 2)
+ " | Z: " + String(zAvg, 2);
if (!inSquat) status += " -> STANDING";
else if (caving) status += " -> SQUATTING WHILE CAVING!";
else status += " -> SQUAT (good)";
// Always print to Serial (when on laptop)
Serial.println(status);
// Send status to BLE only once per second (so it doesn't flood)
if (millis() - lastBLEStatus > BLE_STATUS_INTERVAL) {
if (deviceConnected) {
pTxCharacteristic->setValue((status + "\n").c_str());
pTxCharacteristic->notify();
}
lastBLEStatus = millis();
}
// --- Fault 1: knee caving inward (needs consecutive confirmation) ---
if (caveStreak >= CAVE_CONFIRM_COUNT) {
sendBoth(">>> KNEE CAVING INWARD!\n");
beepInward();
caveStreak = 0;
}
// --- Fault 2: flex sensor bent too far ---
if ( (flexRaw > FLEX_UPPER_LIMIT) || (flexRaw < FLEX_LOWER_LIMIT) ) {
sendBoth(">>> OVER-BEND! Come back up.\n");
buzzOverBend();
}
delay(50);
}
#include <Adafruit_LSM6DS3TRC.h>
Adafruit_LSM6DS3TRC lsm6ds;
// ---------- Pins ----------
#define FLEX_PIN 32 // analog input from flex sensor divider
#define BUZZER_PIN 13 // active buzzer
// ---- Squat detection (X axis) ----
const float SQUAT_X_THRESHOLD = 9.0; // X below this = squat, above = standing
// ---- Cave detection (Z axis) ----
const float CAVE_Z_THRESHOLD = -2.0; // while squatting, Z below this = caving
// ---- Flex over-bend (raw values) ----
const int FLEX_UPPER_LIMIT = 2070; // raw reading = bent too far
const int FLEX_LOWER_LIMIT = 320; // raw reading = bent too far (other direction)
// ---- Consecutive-reading confirmation ----
const int CAVE_CONFIRM_COUNT = 2; // caving reads in a row needed to buzz
const int SAMPLES = 5; // light averaging
int caveStreak = 0; // counts consecutive caving readings
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
pinMode(BUZZER_PIN, OUTPUT);
Wire.begin(21, 22);
if (!lsm6ds.begin_I2C()) { Serial.println("Sensor not found!"); while (1) delay(10); }
Serial.println("Ready - start squatting!");
}
// Two short beeps = knee caving inward
void beepInward() {
for (int i = 0; i < 2; i++) {
digitalWrite(BUZZER_PIN, HIGH); delay(120);
digitalWrite(BUZZER_PIN, LOW); delay(120);
}
}
// Long continuous buzz = bent too far
void buzzOverBend() {
digitalWrite(BUZZER_PIN, HIGH);
delay(600);
digitalWrite(BUZZER_PIN, LOW);
}
void loop() {
// --- Flex sensor: raw reading ---
int flexRaw = analogRead(FLEX_PIN);
// --- Accelerometer: averaged X and Z ---
float xAvg = 0, zAvg = 0;
for (int i = 0; i < SAMPLES; i++) {
sensors_event_t a, g, t;
lsm6ds.getEvent(&a, &g, &t);
xAvg += a.acceleration.x;
zAvg += a.acceleration.z;
delay(5);
}
xAvg /= SAMPLES;
zAvg /= SAMPLES;
bool inSquat = (xAvg < SQUAT_X_THRESHOLD);
bool caving = inSquat && (zAvg < CAVE_Z_THRESHOLD);
// --- Track consecutive caving readings ---
if (caving) {
caveStreak++;
} else {
caveStreak = 0;
}
// --- Print everything ---
Serial.print("Flex: "); Serial.print(flexRaw);
Serial.print(" | X: "); Serial.print(xAvg, 2);
Serial.print(" | Z: "); Serial.print(zAvg, 2);
if (!inSquat) Serial.print(" -> STANDING");
else if (caving) Serial.print(" -> SQUATTING WHILE CAVING!");
else Serial.print(" -> SQUAT (good)");
Serial.println();
// --- Fault 1: knee caving inward (needs consecutive confirmation) ---
if (caveStreak >= CAVE_CONFIRM_COUNT) {
Serial.println(">>> KNEE CAVING INWARD!");
beepInward();
caveStreak = 0;
}
// --- Fault 2: flex sensor bent too far ---
if ( (flexRaw > FLEX_UPPER_LIMIT) || (flexRaw < FLEX_LOWER_LIMIT) ) {
Serial.println(">>> OVER-BEND! Come back up.");
buzzOverBend();
}
delay(50);
}
#include <Adafruit_LSM6DS3TRC.h>
#include <Adafruit_LIS3MDL.h>
Adafruit_LSM6DS3TRC lsm6ds;
Adafruit_LIS3MDL lis3mdl;
// ---------- Pins ----------
#define FLEX_PIN 32 // analog input from flex sensor divider
#define BUZZER_PIN 13 // active buzzer
// ---------- Thresholds (RAW values, no angle math) ----------
const int FLEX_UPPER_LIMIT = 2070; // raw reading that means "bent too far"
const int FLEX_LOWER_LIMIT = 700; // raw reading that means "bent too far"
const float INWARD_THRESHOLD = 5.0; // accelerometer side-tilt
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
pinMode(BUZZER_PIN, OUTPUT);
Wire.begin(21, 22);
if (!lsm6ds.begin_I2C()) {
Serial.println("LSM6DS3TR-C not found - check wiring!");
while (1) delay(10);
}
if (!lis3mdl.begin_I2C()) {
Serial.println("LIS3MDL not found - check wiring!");
while (1) delay(10);
}
Serial.println("Sensors ready.");
}
// Long continuous buzz = bent too far
void buzzOverBend() {
digitalWrite(BUZZER_PIN, HIGH);
delay(600);
digitalWrite(BUZZER_PIN, LOW);
}
// Two short beeps = knee caving inward
void beepInward() {
for (int i = 0; i < 2; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(120);
digitalWrite(BUZZER_PIN, LOW);
delay(120);
}
}
void loop() {
// --- Flex sensor: raw reading, no conversion ---
int flexRaw = analogRead(FLEX_PIN);
// --- Accelerometer: side-to-side tilt ---
sensors_event_t accel, gyro, temp;
lsm6ds.getEvent(&accel, &gyro, &temp);
float sideTilt = accel.acceleration.x;
// --- Print both so you can see what's happening ---
Serial.print("Flex raw: "); Serial.print(flexRaw);
Serial.print(" | SideTilt: "); Serial.print(sideTilt);
Serial.println(" m/s^2");
// --- Trigger 1: flex sensor bent past the raw limit ---
if ( (flexRaw > FLEX_UPPER_LIMIT) || (flexRaw < FLEX_LOWER_LIMIT) ) {
Serial.println(">>> OVER-BEND! Come back up.");
buzzOverBend();
}
// --- Trigger 2: knee caving inward (already working) ---
if (sideTilt > INWARD_THRESHOLD) {
Serial.println(">>> KNEE CAVING INWARD! Fix form.");
beepInward();
}
delay(100);
}

| Part | Note | Price | Link |
|---|---|---|---|
| 2.1 x 3.2 in Breadboard | Wiring together the microcontroller and sensors | $2.99 | Link |
| ESP32 CP2102 | Microcontroller allowing all the sensors to communicate (WiFi, Bluetooth) | $9.39 | Link |
| Power bank | External power for the system | $9.99 | Link |
| STEMMA QT / Qwiic JST SH 4-pin cable (50mm) | Connect Adafruit sensor to ESP-32 | $0.95 | Link |
| 10K Ohm Resistor | Provide current resistance to flex sensor | $0.75 | Link |
| Adafruit LSM6DS3TR-C + LIS3MDL | Accelerometer + gyroscope to detect knee caving or bad form | $19.95 | Link |
| Electromagnetic Active Buzzer 5V | Make a beep sound when bad form is detected or lowest squat position is reached | $0.95 | Link |
| Jumper Wires | Connect all the sensors, buzzers, and controllers via the breadboard | $1.95 | Link |
| Knee Compression Sleeve | Mount all electronic equipment to straddle the knee on this | $12.99 | Link |
| Flex Sensor 4.5” | Detect a bend and send current to the buzzer when a specific bend is reached | $12.95 | Link |
| Neoprene fabric | Used to sew all the components down to the knee sleeve | $25.99 | Link |