Build a posture coach that learns how you sit

Sit up straight for ten seconds and the coach learns what your good posture looks like. Then it quietly watches, and when you slouch over your homework or your phone for too long, it buzzes a gentle reminder.
  • Level:Intermediate
  • Duration: 1 h
  • Ages 10+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a posture coach that learns how you sit – EEMENTOR STEM™ lesson

Many of us spend hours bent over a phone, a laptop or homework, and our neck and back don't like it. A posture coach can help, but everybody sits differently, so a fixed rule like "never lean more than 10 degrees" doesn't work for everyone. Instead, this coach learns from you. You clip a motion sensor to the back of your collar, sit the way you want to sit, and press the button. For ten seconds the UNO collects measurements and works out the average angle and how much you naturally move. From then on it knows what "normal" is for you, and when your posture stays far from normal for 20 seconds, it buzzes.

What you'll learn

  • How to calculate tilt angles from the accelerometer in the MPU-6050
  • How a model of "normal" is learned from examples: the mean and the standard deviation
  • How anomaly detection works: measuring how unusual a new reading is
  • How to store the learned model in EEPROM

What you need

  • UNO R3 board and USB cable (UNO R3 Starter Kit)
  • GY-521 MPU-6050 module (45-in-1 Sensor Kit)
  • Active buzzer (UNO R3 Starter Kit)
  • Green LED, red LED and 2 resistors, 220 Ω (UNO R3 Starter Kit)
  • 1 push button (UNO R3 Starter Kit)
  • 4 long jumper wires (join two together if they are too short) and a clothes peg or tape
  • Breadboard and jumper wires (UNO R3 Starter Kit)

Wiring

Breadboard wiring diagram: Build a posture coach that learns how you sit
Breadboard view. Check each connection against the wiring table below.
From To
MPU-6050 VCC 5V
MPU-6050 GND GND
MPU-6050 SCL UNO A5
MPU-6050 SDA UNO A4
UNO pin 6 220 Ω resistor, then the green LED's long leg
UNO pin 7 220 Ω resistor, then the red LED's long leg
Both LEDs' short legs GND
Active buzzer + (longer leg) UNO pin 8
Active buzzer − GND
Push button, one leg UNO pin 2
Push button, the diagonally opposite leg GND

Build it on the breadboard first. To wear it, connect the MPU-6050 with four long wires and clip it to the back of your shirt collar with a clothes peg, chip side out. It must not slip: if it moves, the coach has to learn again.

The sketch

Wire and EEPROM come with the Arduino IDE. The sketch talks to the MPU-6050 directly, so you don't need a library.

#include <Wire.h>
#include <EEPROM.h>

const int MPU = 0x68;                    // I2C address of the MPU-6050
const int BUTTON_PIN = 2, GREEN_PIN = 6, RED_PIN = 7, BUZZER_PIN = 8;
const float MIN_TOLERANCE = 8.0;         // degrees: always allow at least this much
const unsigned long SLOUCH_TIME = 20000; // buzz after 20 s of bad posture
const unsigned long REPEAT_TIME = 10000; // and again every 10 s while it stays bad

struct Model {                           // what the coach learned about you
  byte marker;                           // 0x42 = a model is saved
  float pitch, roll;                     // your average angles
  float pitchTol, rollTol;               // how far you may move
};
Model model;
bool trained = false;
float pitch = 0, roll = 0;               // smoothed angles
unsigned long badSince = 0, lastBuzz = 0, lastPrint = 0, minuteStart = 0;
unsigned long goodSamples = 0, allSamples = 0;

void readAngles() {
  Wire.beginTransmission(MPU);
  Wire.write(0x3B);                      // first acceleration register
  Wire.endTransmission(false);
  Wire.requestFrom(MPU, 6);
  int16_t ax = Wire.read() << 8;        // high byte first, then the low byte
  ax |= Wire.read();
  int16_t ay = Wire.read() << 8;
  ay |= Wire.read();
  int16_t az = Wire.read() << 8;
  az |= Wire.read();
  // Gravity tells us which way is down: turn that into two angles
  float p = atan2(ax, sqrt((float)ay * ay + (float)az * az)) * 180 / PI;
  float r = atan2(ay, az) * 180 / PI;
  pitch += 0.15 * (p - pitch);           // smooth out small shakes
  roll += 0.15 * (r - roll);
}

void beep(int ms) {
  digitalWrite(BUZZER_PIN, HIGH);
  delay(ms);
  digitalWrite(BUZZER_PIN, LOW);
}

void learn() {
  Serial.println("Sit the way you want to sit... learning starts in 3 s");
  for (int i = 0; i < 3; i++) {
    beep(60);
    delay(940);
  }
  float sumP = 0, sumR = 0, sumP2 = 0, sumR2 = 0;
  const int N = 200;                     // 200 samples in 10 seconds
  for (int i = 0; i < N; i++) {
    readAngles();
    sumP += pitch; sumP2 += pitch * pitch;
    sumR += roll;  sumR2 += roll * roll;
    digitalWrite(GREEN_PIN, i % 10 < 5);
    delay(50);
  }
  model.pitch = sumP / N;
  model.roll = sumR / N;
  float sdP = sqrt(max(sumP2 / N - model.pitch * model.pitch, 0.0f));   // standard deviation
  float sdR = sqrt(max(sumR2 / N - model.roll * model.roll, 0.0f));
  model.pitchTol = max(3 * sdP, MIN_TOLERANCE);
  model.rollTol = max(3 * sdR, MIN_TOLERANCE);
  model.marker = 0x42;
  EEPROM.put(0, model);
  trained = true;
  badSince = 0;
  beep(300);
  Serial.print("Learned: pitch ");
  Serial.print(model.pitch, 1);
  Serial.print(" +/- ");
  Serial.print(model.pitchTol, 1);
  Serial.print(", roll ");
  Serial.print(model.roll, 1);
  Serial.print(" +/- ");
  Serial.println(model.rollTol, 1);
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(RED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  Wire.begin();
  Wire.beginTransmission(MPU);
  Wire.write(0x6B);                      // power register: wake the sensor up
  Wire.write(0);
  Wire.endTransmission();
  for (int i = 0; i < 30; i++) readAngles();   // let the smoothing settle
  EEPROM.get(0, model);
  trained = model.marker == 0x42;
  Serial.println(trained ? "Using the posture it learned before. Press the button to learn again."
                         : "Press the button and sit up straight to teach me your good posture.");
  minuteStart = millis();
}

void loop() {
  if (digitalRead(BUTTON_PIN) == LOW) learn();
  readAngles();
  unsigned long now = millis();

  if (!trained) {                        // waiting to learn: both LEDs blink
    digitalWrite(GREEN_PIN, (now / 500) % 2);
    digitalWrite(RED_PIN, (now / 500) % 2);
    delay(50);
    return;
  }

  // How unusual is this posture? 1.0 = right at the edge of what you taught it
  float score = max(fabs(pitch - model.pitch) / model.pitchTol, fabs(roll - model.roll) / model.rollTol);
  bool good = score <= 1.0;
  digitalWrite(GREEN_PIN, good);
  digitalWrite(RED_PIN, !good);
  allSamples++;
  if (good) goodSamples++;

  if (good) badSince = 0;
  else if (badSince == 0) badSince = now;
  if (badSince != 0 && now - badSince > SLOUCH_TIME && now - lastBuzz > REPEAT_TIME) {
    lastBuzz = now;
    beep(80);
    delay(120);
    beep(80);
    Serial.println("Time to sit up!");
  }

  if (now - lastPrint > 500) {
    lastPrint = now;
    Serial.print("Pitch ");
    Serial.print(pitch, 1);
    Serial.print("  Roll ");
    Serial.print(roll, 1);
    Serial.print("  Score ");
    Serial.println(score, 2);
  }
  if (now - minuteStart > 60000) {       // a small report every minute
    Serial.print("Good posture in the last minute: ");
    Serial.print(100 * goodSamples / allSamples);
    Serial.println(" %");
    goodSamples = allSamples = 0;
    minuteStart = now;
  }
  delay(50);
}

Try it

  1. Upload the sketch and open the Serial Monitor at 9600 baud. Both LEDs blink: the coach hasn't learned anything yet.
  2. Hold the sensor upright in your hand (or clip it to your collar) and sit up straight. Press the button. After three short beeps, stay still for 10 seconds while the green LED blinks. A long beep means it has learned.
  3. Look at the Serial Monitor: it shows the angles it learned and how much you may move, for example pitch 4.2 +/- 8.0.
  4. Lean forward over the table as if you were reading your phone. The score goes above 1.0 and the red LED lights. After 20 seconds you get two short beeps, and again every 10 seconds until you sit up.
  5. Press the button again any time to teach it a new posture. The model is saved, so it remembers you after a restart.

How it works

The MPU-6050 measures acceleration in three directions. When it is not moving, the only acceleration is gravity, pointing down. If you tilt the sensor, gravity is shared differently between the three directions, and atan2() turns that into two angles: pitch (leaning forward and back) and roll (leaning sideways).

Learning is two simple statistics. The mean (average) of 200 readings is your normal angle. The standard deviation says how much the readings naturally wobble around that average. Together they are a small model of your good posture. The coach allows three standard deviations, but at least 8 degrees, because nobody sits perfectly still.

Every new reading gets a score: how far it is from your average, measured in "allowed distances". Below 1.0 the posture looks like what you taught it; above 1.0 it is unusual. This is called anomaly detection, and it is used everywhere: banks use it to spot strange card payments, and factories to spot machines that start to vibrate differently. The trick is the same: learn what normal looks like, then notice what doesn't fit. Finally, one bad moment doesn't count; only 20 seconds of bad posture trigger a reminder, so reaching for a pencil doesn't make it beep.

Try this next

  • Let the coach learn a little all the time: when the score is very low, move the model's average 1 % towards the new reading.
  • Show the percentage of good posture per minute as a bar on the 1602 LCD.
  • Use the passive buzzer and play a short happy tune after five minutes of good posture.
  • Rebuild it on the ESP32 with the ESP32's own web page, showing your posture score for the day.

Troubleshooting

  • Pitch and roll stay at 0 or show nonsense: check SDA on A4 and SCL on A5. If it still fails, the module may need 3.3 V: try VCC on 3V3.
  • It beeps all the time: the sensor moved after learning. Fix it firmly and press the button to learn again.
  • It never beeps: the learned tolerance is too big because you moved while it was learning. Learn again and sit very still for the 10 seconds.
  • The red LED flickers between good and bad: you are right at the edge. That's fine: only 20 seconds of bad posture in a row make it beep.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit