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

| 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
- Upload the sketch and open the Serial Monitor at 9600 baud. Both LEDs blink: the coach hasn't learned anything yet.
- 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.
- Look at the Serial Monitor: it shows the angles it learned and how much you may move, for example
pitch 4.2 +/- 8.0. - 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.
- 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.