Ritwik_BlueStamp_Portfolio

Knee Rehabilitation Device - BSE Summer


Project maintained by ritwikc35 Hosted on GitHub Pages — Theme by mattgraham

Knee Rehabilitation Device

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

Headshot

Modifications

Modifications Descripton

Struggles/Challenges

Code

#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);
}

Final Milestone

Milestone 3 Descripton

Struggles/Challenges

Modification ideas

#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);
}

Second Milestone

Milestone 2 Descripton

Struggles/Challenges

Milestone 3 Ideas

Second Milestone Code

#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);
}

First Milestone

Milestone 1 Descripton

Struggles/Challenges

Milestone 2 Ideas

First Milestone Code

#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);
}

Starter Project

Starter Project Description

Struggles/Challenges

Schematic

Schematic

Labels:

  1. ESP-32 Microcontroller. This microcontroller is the brains of the entire circuit, and it bridges the gap of the communication between the laptop delivering the code and the other sensors/parts in the circuit.
  2. Adafruit Adafruit LSM6DS3TR-C + LIS3MDL (IMU - Gyroscope + Accelerometer). This sensor tracks translational acceleration and position in three axes, and the gyroscope measures angular velocity in three axes. This was used to determine whether the knee is caving inwards during a squat or not, using the IMU.
  3. Flex Sensor. This sensor has particles lined up within the length of the body, and as the flex sensor bends, the particles’ distance between each other increases. When this distance increases, the resistance pulling these particles back together also increases. The sensor tracks this resistance, and determines the value for the flex sensor dependent on how bent it is.
  4. Active Buzzer. Gets current from breadboard and ESP-32 to, when prompted, create a beeping sound. This sound cannot be altered pitch or frequency wise, however what can be altered is the buzzing pattern. I used this to differentiate the improper form caused by knee caving versus over-bend.
  5. 10k Ω Resistor. This resistor is used to limit the current from the ESP-32 to the Flex Sensor to prevent a short circuit.
  6. STEMMA QT / Qwiic JST SH 4-pin cable. This cable is used to connect into the Adafruit IMU. It has four cables - Red, Black, Blue, and Yellow. Red goes to power (3V3), Black goes to ground (GND), and Blue and Yellow go to the D21 and D22 pinouts in the microcontroller.
  7. LEDs. These Red and Green LEDs are used to provide visual feedback to the user and are a part of my modifications. They are wired to Ground and Pinout, and they require 560 Ohm resistors.
  8. Button. This button sets the levels for the difficulty of each squat.

Bill of Materials

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

Other Resources + Links for helpful stuff: