Posture and Stillness Monitoring System using MPU6050 with Seeed XIAO SAMD21
Table of Contents ▼
01 Overview:
This project implements a wearable posture monitoring system using a Seeed XIAO SAMD21 microcontroller combined with an MPU6050 6-axis accelerometer/gyroscope module. The system continuously tracks the wearer's back angle relative to a calibrated upright reference position and alerts the user through a severity-scaled buzzer tone whenever sustained slouching combined with physical stillness is detected, encouraging better posture during long periods at a desk.
02 Hardware and Software Components
Hardware Components
| Component | Description |
|---|---|
| Seeed XIAO SAMD21 | Main microcontroller unit used to run the posture detection logic and drive the buzzer and LED outputs. |
| MPU6050 Sensor | 6-axis accelerometer/gyroscope module used over I2C to calculate posture angle and detect movement/stillness. |
| Passive Buzzer | Driven from pin A0 via the SAMD21's DAC output, produces a severity-scaled audible alert through a transistor stage when sustained slouching is detected. |
| Status LED | Connected to pin D3, gives a simple visual indicator of slouching state at all times. |
| NPN Transistor | Switching stage used to drive the buzzer, since it draws more current than a GPIO/DAC pin can supply directly. |
| Resistors | Base current-limiting resistor for the transistor stage, and a current-limiting resistor for the status LED. |
| Breadboard | Prototyping base that holds components in place and provides shared power/ground rails, with no soldering required. |
| Jumper Wires | Flexible conductive wires used to establish secure electrical connections between components and GPIO pins. |
| Battery | Portable power source that lets the wearable unit run untethered during normal use. |
Software Components
| Software / Library | Description |
|---|---|
| Arduino IDE | Development environment used to write, compile, and upload sketches to the microcontroller. |
| Seeed SAMD21 Board Package | Board support package installed via Boards Manager, enabling the IDE to compile and upload to the XIAO SAMD21. |
Libraries Used
- Wire.h: Arduino library for I2C communication, used to read accelerometer data from the MPU6050.
- Adafruit MPU6050: Requires also installing Adafruit Unified Sensor (its dependency). This was the one that failed to detect your specific clone module earlier.
- Seeed SAMD Boards: the Arduino IDE doesn't natively recognize the XIAO SAMD21, so installing it adds the pin mappings, compiler support, and bootloader definitions required to compile and upload code to this specific board.
03 Application Description
The system reads accelerometer data from the MPU6050 over I2C and calculates the wearer's current posture angle using the Y and Z acceleration components. This angle is compared against an upright reference angle established during a calibration routine at startup. If the difference exceeds a set threshold, the wearer is considered to be slouching.
To avoid false alerts from momentary posture changes, the system also tracks how long the slouch has continued and whether the wearer has been physically still, using the change in total acceleration magnitude between readings as a proxy for movement. An alert is only triggered when both conditions are met: continuous slouching past a set duration, and a minimum number of consecutive "no movement" readings.
When triggered, the buzzer sounds a tone whose pitch and pattern scale with how severe the slouch is: a short mild tone for a small deviation, a longer tone for a moderate slouch, and a double-beep for a severe slouch. The alert stops as soon as the wearer returns to an upright posture. The status LED provides a simpler, immediate visual indicator of slouch state, updating independently of the alert timers.
MPU6050 - 6-Axis Accelerometer/Gyroscope
The MPU6050 is a low-cost 6-axis motion tracking module combining a 3-axis accelerometer and a 3-axis gyroscope on a single chip, communicating over I2C. In this project, only the accelerometer readings are used, since the wearer's posture angle can be derived directly from the direction of gravity relative to the sensor.
Specifications
- Communication interface: I2C (default address 0x68)
- Accelerometer range used: ±2g
- Accelerometer sensitivity: 16384 LSB/g at ±2g range
- Supply voltage: 3-5V (module dependent)
04 Hardware Setup
Circuit Diagram:
The MPU6050 connects to the XIAO SAMD21 over I2C, with SDA and SCL wired to the corresponding I2C pins, and VCC/GND wired to the board's power rails.
The passive buzzer is driven through a transistor switching stage rather than directly from the DAC pin, since it draws more current than the SAMD21's A0/DAC output can safely supply: pin A0 connects through a 1k resistor to the transistor's base, the buzzer sits between the collector and the supply voltage, and the emitter ties to ground. This lets the DAC output a synthesized sine wave tone through the transistor stage instead of a simple on/off square wave. The status LED is wired to pin D3 through a standard current-limiting resistor, with the cathode to ground.
Pin Configuration
| Pin | Connection |
|---|---|
| A0 | Passive buzzer (via transistor, DAC/analogWrite sine output) |
| D3 | Status LED |
| SDA / SCL | MPU6050 I2C data/clock lines |
| VCC / GND | Power and ground rails, shared across all components |
05 Software Setup
Before uploading the sketch, prepare the Arduino IDE as follows:
- Install the Arduino IDE (see Software Components in Section 02) if it is not yet installed on your computer.
- Install the Seeed SAMD21 board package via Tools > Board > Boards Manager, searching for "Seeed SAMD" and installing it.
- Wire.h, used for I2C communication, is bundled with the Arduino IDE by default, so no separate installation is needed.
- Connect the XIAO SAMD21 to your computer and select the correct board and COM port under Tools. Note that the port can change after each upload or reset on this board, so re-select it if the Serial Monitor stops responding.
- Set the Serial Monitor's baud rate to 115200 to match
Serial.begin(115200)in the sketch.
06 Code
#include <Wire.h>
#define MPU6050_ADDR 0x68
#define PWR_MGMT_1 0x6B
#define ACCEL_CONFIG 0x1C
#define ACCEL_XOUT_H 0x3B
const int ledSignal = 3; // D3 - status LED
const int Buzzer = A0; // A0 - DAC output -> 1k -> transistor base
float accelX = 0, accelY = 0, accelZ = 0;
float uprightAngle = 0;
float postureAngle = 0;
float previousAccel = 1.0;
unsigned long slouchStartTime = 0;
bool wasSlouching = false;
// Settings
const float SLOUCH_THRESHOLD = 15.0;
const float MOVEMENT_THRESHOLD = 0.15;
const unsigned long SLOUCH_LIMIT = 10000UL; // 10 s (use 60000UL for 1 minute)
const int STILLNESS_COUNT_THRESHOLD = 5;
int stillnessCounter = 0;
// ------------------------------------------------
// Severity settings
// ------------------------------------------------
const float SEVERE_ANGLE = 45.0;
const float MODERATE_ANGLE = 25.0;
const float SEVERE_TIER_ANGLE = 35.0;
const unsigned int BUZZER_FREQ_MIN_HZ = 1000;
const unsigned int BUZZER_FREQ_MAX_HZ = 3500;
enum Severity { SEV_NONE, SEV_MILD, SEV_MODERATE, SEV_SEVERE };
// ------------------------------------------------
// DAC sine wave
// ------------------------------------------------
const int DAC_BITS = 10; // SAMD21 DAC is 10-bit (0..1023)
const int SINE_SAMPLES = 16; // samples per cycle (16 keeps timing achievable up to 3.5 kHz)
uint16_t sineTable[SINE_SAMPLES];
void buildSineTable()
{
const float mid = ((1 << DAC_BITS) - 1) / 2.0; // 511.5
for (int i = 0; i < SINE_SAMPLES; i++)
{
sineTable[i] = (uint16_t)(mid + mid * sin(2.0 * PI * i / SINE_SAMPLES));
}
}
Severity getSeverity(float angleDiff)
{
if (angleDiff < SLOUCH_THRESHOLD) return SEV_NONE;
if (angleDiff < MODERATE_ANGLE) return SEV_MILD;
if (angleDiff < SEVERE_TIER_ANGLE) return SEV_MODERATE;
return SEV_SEVERE;
}
const char* severityName(Severity s)
{
switch (s)
{
case SEV_MILD: return "MILD";
case SEV_MODERATE: return "MODERATE";
case SEV_SEVERE: return "SEVERE";
default: return "NONE";
}
}
unsigned int getSeverityFrequency(float angleDiff)
{
float t = (angleDiff - SLOUCH_THRESHOLD) / (SEVERE_ANGLE - SLOUCH_THRESHOLD);
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
return (unsigned int)(BUZZER_FREQ_MIN_HZ + t * (BUZZER_FREQ_MAX_HZ - BUZZER_FREQ_MIN_HZ));
}
// Plays a sine tone of freqHz for durationMs using the DAC on A0.
// Blocking. Sample times are scheduled against micros() so small
// analogWrite() overheads don't accumulate into pitch drift.
void buzzFor(unsigned int freqHz, unsigned long durationMs)
{
unsigned long stepUs = 1000000UL / ((unsigned long)freqHz * SINE_SAMPLES);
unsigned long totalUs = durationMs * 1000UL;
unsigned long start = micros();
unsigned long nextUs = start;
int idx = 0;
while ((micros() - start) < totalUs)
{
analogWrite(Buzzer, sineTable[idx]);
idx++;
if (idx >= SINE_SAMPLES) idx = 0;
nextUs += stepUs;
while ((long)(micros() - nextUs) < 0) { } // wait for next sample slot
}
analogWrite(Buzzer, 0); // DAC to 0 V = silent
}
void playAlert(Severity sev, unsigned int freqHz)
{
switch (sev)
{
case SEV_MILD:
buzzFor(freqHz, 150);
break;
case SEV_MODERATE:
buzzFor(freqHz, 300);
break;
case SEV_SEVERE:
buzzFor(freqHz, 120);
delay(60);
buzzFor(freqHz, 120);
break;
default:
break;
}
}
void writeMPU(byte reg, byte data)
{
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(reg);
Wire.write(data);
Wire.endTransmission();
}
bool readAccelerometer()
{
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(ACCEL_XOUT_H);
if (Wire.endTransmission(false) != 0)
{
return false;
}
uint8_t received = Wire.requestFrom((uint8_t)MPU6050_ADDR, (uint8_t)6, (uint8_t)true);
if (received < 6 || Wire.available() < 6)
{
return false;
}
int16_t rawX = (int16_t)(Wire.read() << 8);
rawX |= Wire.read();
int16_t rawY = (int16_t)(Wire.read() << 8);
rawY |= Wire.read();
int16_t rawZ = (int16_t)(Wire.read() << 8);
rawZ |= Wire.read();
accelX = rawX / 16384.0;
accelY = rawY / 16384.0;
accelZ = rawZ / 16384.0;
return true;
}
float getPostureAngle()
{
return atan2(accelZ, -accelX) * 180.0 / PI;
}
float getAccelerationMagnitude()
{
return sqrt(accelX * accelX + accelY * accelY + accelZ * accelZ);
}
void calibrate()
{
Serial.println();
Serial.println("====================================");
Serial.println("UPRIGHT CALIBRATION");
Serial.println("Please sit upright and stay still.");
Serial.println("Calibration starting...");
Serial.println("====================================");
delay(3000);
float totalAngle = 0;
int validSamples = 0;
for (int i = 0; i < 100; i++)
{
if (readAccelerometer())
{
totalAngle += getPostureAngle();
validSamples++;
}
delay(20);
}
if (validSamples > 0)
{
uprightAngle = totalAngle / validSamples;
}
else
{
Serial.println("Calibration failed: no valid sensor readings.");
Serial.println("Check the MPU6050 wiring and restart.");
while (1);
}
Serial.println();
Serial.println("Calibration complete!");
Serial.print("Upright reference angle: ");
Serial.print(uprightAngle, 2);
Serial.println(" degrees");
Serial.println();
Serial.println("Starting posture monitoring...");
Serial.println();
}
void setup()
{
Serial.begin(115200);
delay(2000);
Serial.println("BOOT OK");
pinMode(ledSignal, OUTPUT);
digitalWrite(ledSignal, LOW);
// DAC setup: 10-bit resolution, then park the output at 0 V
analogWriteResolution(DAC_BITS);
analogWrite(Buzzer, 0);
buildSineTable();
Wire.begin();
writeMPU(PWR_MGMT_1, 0x00);
writeMPU(ACCEL_CONFIG, 0x00);
delay(1000);
Serial.println("====================================");
Serial.println("XIAO + MPU6050 - POSTURE + STILLNESS (DAC buzzer)");
Serial.println("====================================");
Wire.beginTransmission(MPU6050_ADDR);
if (Wire.endTransmission() == 0)
{
Serial.println("MPU6050: CONNECTED");
}
else
{
Serial.println("MPU6050: NOT FOUND");
Serial.println("Check VCC, GND, SDA and SCL.");
while (1);
}
calibrate();
readAccelerometer();
previousAccel = getAccelerationMagnitude();
}
void loop()
{
if (!readAccelerometer())
{
Serial.println("Sensor read failed - check wiring.");
analogWrite(Buzzer, 0);
delay(1000);
return;
}
postureAngle = getPostureAngle();
float angleDifference = postureAngle - uprightAngle;
while (angleDifference > 180.0) angleDifference -= 360.0;
while (angleDifference < -180.0) angleDifference += 360.0;
angleDifference = fabs(angleDifference);
bool slouching = (angleDifference >= SLOUCH_THRESHOLD);
Severity severity = getSeverity(angleDifference);
float currentAccel = getAccelerationMagnitude();
float accelerationChange = fabs(currentAccel - previousAccel);
bool movementDetected = false;
if (accelerationChange >= MOVEMENT_THRESHOLD)
{
movementDetected = true;
stillnessCounter = 0;
}
else if (stillnessCounter < 10000)
{
stillnessCounter++;
}
previousAccel = currentAccel;
// --- Slouch streak tracking ---
if (slouching && !wasSlouching)
{
slouchStartTime = millis();
}
if (!slouching)
{
stillnessCounter = 0;
}
wasSlouching = slouching;
// --- Alert decision ---
unsigned long slouchElapsed = 0;
bool triggerAlert = false;
if (slouching)
{
slouchElapsed = millis() - slouchStartTime;
if (slouchElapsed >= SLOUCH_LIMIT && stillnessCounter >= STILLNESS_COUNT_THRESHOLD)
{
triggerAlert = true;
}
}
unsigned int alertFreq = slouching ? getSeverityFrequency(angleDifference) : 0;
digitalWrite(ledSignal, slouching ? HIGH : LOW);
// Serial output (printed BEFORE the blocking beep)
Serial.print("X: ");
Serial.print(accelX, 2);
Serial.print(" | Y: ");
Serial.print(accelY, 2);
Serial.print(" | Z: ");
Serial.print(accelZ, 2);
Serial.print(" | Angle: ");
Serial.print(postureAngle, 1);
Serial.print("deg | Diff: ");
Serial.print(angleDifference, 1);
Serial.print("deg | Movement: ");
Serial.print(movementDetected ? "YES" : "NO");
Serial.print(" | Still count: ");
Serial.print(stillnessCounter);
Serial.print("/");
Serial.print(STILLNESS_COUNT_THRESHOLD);
Serial.print(" | Posture: ");
Serial.print(slouching ? "SLOUCHING" : "UPRIGHT");
Serial.print(" | Severity: ");
Serial.print(severityName(severity));
Serial.print(" | Slouch time: ");
Serial.print(slouching ? (slouchElapsed / 1000UL) : 0UL);
Serial.print("s | Buzzer: ");
Serial.print(triggerAlert ? "ON" : "OFF");
Serial.print(" @ ");
Serial.print(triggerAlert ? alertFreq : 0);
Serial.println("Hz");
if (triggerAlert)
{
playAlert(severity, alertFreq);
}
else
{
analogWrite(Buzzer, 0);
}
delay(1000);
}
07 Code Breakdown
Here is what each part of the sketch does. Read this after uploading the code.
Library Import & Pin Setup
#include <Wire.h>
#define MPU6050_ADDR 0x68
#define PWR_MGMT_1 0x6B
#define ACCEL_CONFIG 0x1C
#define ACCEL_XOUT_H 0x3B
const int MOTOR_PIN = A0;
const int ledSignal = 3;
const int Buzzer = 2;
Wire.h provides the I2C protocol used to communicate with the MPU6050. The register address defines identify the sensor's I2C address and the specific registers used to wake it up, configure its accelerometer range, and read acceleration data. MOTOR_PIN, ledSignal, and Buzzer assign the physical pins used for the vibration motor, status LED, and buzzer respectively.
Global Variables & Settings
float accelX = 0, accelY = 0, accelZ = 0;
float uprightAngle = 0;
float postureAngle = 0;
float previousAccel = 1.0;
unsigned long slouchStartTime = 0;
bool wasSlouching = false;
bool motorOn = false;
const float SLOUCH_THRESHOLD = 30.0;
const float MOVEMENT_THRESHOLD = 0.15;
const unsigned long SLOUCH_LIMIT = 10000UL;
const int STILLNESS_COUNT_THRESHOLD = 5;
int stillnessCounter = 0;
const unsigned int BUZZER_FREQ_HZ = 2000;
These variables hold the live accelerometer readings, the calibrated upright reference angle, and the state used to track slouch duration and stillness. SLOUCH_THRESHOLD sets how many degrees of tilt count as slouching, SLOUCH_LIMIT sets how long that slouch must continue before an alert fires, MOVEMENT_THRESHOLD sets the sensitivity of the stillness check, and BUZZER_FREQ_HZ sets the buzzer's tone pitch.
MPU6050 Communication
void writeMPU(byte reg, byte data)
{
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(reg);
Wire.write(data);
Wire.endTransmission();
}
bool readAccelerometer()
{
Wire.beginTransmission(MPU6050_ADDR);
Wire.write(ACCEL_XOUT_H);
if (Wire.endTransmission(false) != 0)
{
return false;
}
uint8_t received = Wire.requestFrom((uint8_t)MPU6050_ADDR, (uint8_t)6, (uint8_t)true);
if (received < 6 || Wire.available() < 6)
{
return false;
}
int16_t rawX = (int16_t)(Wire.read() << 8);
rawX |= Wire.read();
int16_t rawY = (int16_t)(Wire.read() << 8);
rawY |= Wire.read();
int16_t rawZ = (int16_t)(Wire.read() << 8);
rawZ |= Wire.read();
accelX = rawX / 16384.0;
accelY = rawY / 16384.0;
accelZ = rawZ / 16384.0;
return true;
}
writeMPU() sends a configuration byte to a given register over I2C, used during setup to wake the sensor and set its accelerometer range. readAccelerometer() requests six bytes from the sensor's accelerometer output registers and reconstructs the X, Y, and Z acceleration values as signed 16-bit integers, scaling each by 16384.0 (the LSB/g sensitivity at ±2g) to get values in g-units. It returns false if the I2C transaction doesn't complete or doesn't return the expected 6 bytes, so the main loop can skip that cycle instead of acting on bad data.
Posture Angle & Acceleration Magnitude
float getPostureAngle()
{
return atan2(accelZ, -accelY) * 180.0 / PI;
}
float getAccelerationMagnitude()
{
return sqrt(accelX * accelX + accelY * accelY + accelZ * accelZ);
}
getPostureAngle() uses atan2() on the Z and Y acceleration components to compute the current tilt angle in degrees, based on how gravity is distributed across those two axes as the wearer leans forward or sits upright. getAccelerationMagnitude() computes the total acceleration vector length, which stays close to 1g when the sensor is still and changes noticeably during movement, making it a simple proxy for detecting motion.
Calibration Routine
void calibrate()
{
Serial.println();
Serial.println("====================================");
Serial.println("UPRIGHT CALIBRATION");
Serial.println("Please sit upright and stay still.");
Serial.println("Calibration starting...");
Serial.println("====================================");
delay(3000);
float totalAngle = 0;
int validSamples = 0;
for (int i = 0; i < 100; i++)
{
if (readAccelerometer())
{
totalAngle += getPostureAngle();
validSamples++;
}
delay(20);
}
if (validSamples > 0)
{
uprightAngle = totalAngle / validSamples;
}
else
{
Serial.println("Calibration failed: no valid sensor readings.");
Serial.println("Check the MPU6050 wiring and restart.");
while (1);
}
Serial.println();
Serial.println("Calibration complete!");
Serial.print("Upright reference angle: ");
Serial.print(uprightAngle, 2);
Serial.println(" degrees");
Serial.println();
Serial.println("Starting posture monitoring...");
Serial.println();
}
calibrate() runs once at startup, giving the wearer 3 seconds to sit upright before averaging 100 valid angle samples to establish a personalized uprightAngle reference, rather than assuming a fixed value that may not match the wearer's actual posture or sensor mounting angle. If no valid samples are collected (e.g. due to a wiring fault), it prints an error and halts rather than continuing with a meaningless reference angle.
Initialization Phase (setup)
void setup()
{
Serial.begin(115200);
delay(2000);
pinMode(Buzzer, OUTPUT);
pinMode(ledSignal, OUTPUT);
noTone(Buzzer);
digitalWrite(ledSignal, LOW);
analogWriteResolution(10);
analogWrite(MOTOR_PIN, 0);
Wire.begin();
writeMPU(PWR_MGMT_1, 0x00);
writeMPU(ACCEL_CONFIG, 0x00);
delay(1000);
Wire.beginTransmission(MPU6050_ADDR);
if (Wire.endTransmission() == 0)
{
Serial.println("MPU6050: CONNECTED");
}
else
{
Serial.println("MPU6050: NOT FOUND");
Serial.println("Check VCC, GND, SDA and SCL.");
while (1);
}
calibrate();
readAccelerometer();
previousAccel = getAccelerationMagnitude();
}
setup() runs once when the board boots. It starts the Serial Monitor at 115200 baud, configures the buzzer, LED, and motor pins, and sets the DAC resolution to 10-bit for the motor's analogWrite() calls. It wakes the MPU6050 and sets its accelerometer range via writeMPU(), then confirms the sensor is responding on the I2C bus. If the MPU6050 isn't found, the sketch halts in an infinite while(1) loop so it never proceeds with a non-functional sensor. Once confirmed, it runs calibrate() and takes an initial acceleration reading to establish a baseline before entering the main loop.
Main Loop - Angle Calculation
if (!readAccelerometer())
{
Serial.println("Sensor read failed - check wiring.");
noTone(Buzzer);
analogWrite(MOTOR_PIN, 0);
delay(1000);
return;
}
postureAngle = getPostureAngle();
float angleDifference = postureAngle - uprightAngle;
while (angleDifference > 180.0) angleDifference -= 360.0;
while (angleDifference < -180.0) angleDifference += 360.0;
angleDifference = fabs(angleDifference);
bool slouching = (angleDifference >= SLOUCH_THRESHOLD);
Each loop pass first attempts to read the sensor; if the read fails, the outputs are turned off and the rest of the loop is skipped for that cycle via return. Otherwise, the current posture angle is calculated and compared against the calibrated uprightAngle. The difference is wrapped into a -180 to 180 degree range before taking its absolute value, which prevents a false "slouch" reading at the point where the angle measurement wraps around from +180 to -180.
Stillness Detection
float currentAccel = getAccelerationMagnitude();
float accelerationChange = fabs(currentAccel - previousAccel);
bool movementDetected = false;
if (accelerationChange >= MOVEMENT_THRESHOLD)
{
movementDetected = true;
stillnessCounter = 0;
}
else if (stillnessCounter < 10000)
{
stillnessCounter++;
}
previousAccel = currentAccel;
The loop compares the current acceleration magnitude against the previous reading. A change at or above MOVEMENT_THRESHOLD counts as movement and resets stillnessCounter to zero; otherwise the counter increments, tracking how many consecutive readings have shown no significant movement. This counter is capped to avoid overflow on very long idle periods.
Alert Decision Logic
if (slouching && !wasSlouching)
{
slouchStartTime = millis();
}
wasSlouching = slouching;
unsigned long slouchElapsed = 0;
bool triggerAlert = false;
if (slouching)
{
slouchElapsed = millis() - slouchStartTime;
if (slouchElapsed >= SLOUCH_LIMIT && stillnessCounter >= STILLNESS_COUNT_THRESHOLD)
{
triggerAlert = true;
}
}
motorOn = triggerAlert;
When the wearer first transitions from upright to slouching, slouchStartTime records the moment it began. An alert only triggers once two conditions are both true: the wearer has been slouching continuously past SLOUCH_LIMIT, and stillnessCounter has reached STILLNESS_COUNT_THRESHOLD. This combination avoids false alerts from someone who leans forward briefly while actively moving, such as reaching for something on the desk.
Output Control & Serial Monitor
digitalWrite(ledSignal, slouching ? HIGH : LOW);
if (triggerAlert)
{
tone(Buzzer, BUZZER_FREQ_HZ);
analogWrite(MOTOR_PIN, 1023);
}
else
{
noTone(Buzzer);
analogWrite(MOTOR_PIN, 0);
}
Serial.print("X: ");
Serial.print(accelX, 2);
Serial.print(" | Posture: ");
Serial.print(slouching ? "SLOUCHING" : "UPRIGHT");
Serial.print(" | Buzzer: ");
Serial.print(motorOn ? "ON" : "OFF");
Serial.print(" | Motor(DAC): ");
Serial.println(motorOn ? "ON" : "OFF");
delay(1000);
The status LED tracks the slouch angle on its own, updating regardless of the alert state. The buzzer and motor, by contrast, only activate together once triggerAlert becomes true, with the buzzer sounding a continuous tone via tone() and the motor running at full DAC output (1023) via analogWrite(). Both turn off immediately once the wearer returns upright. Each loop pass finishes by printing the raw acceleration values, computed angle, movement/stillness status, current posture, elapsed slouch time, and buzzer/motor state to the Serial Monitor, useful for tuning the threshold constants during testing.
08 Testing and Calibration
Once the hardware is wired according to Section 04 and the sketch from Section 06 is uploaded, use the following steps to test and calibrate the system:
- Connection check: Open the Serial Monitor at 115200 baud after uploading. An "MPU6050: CONNECTED" message confirms the sensor is wired correctly and communicating over I2C; "MPU6050: NOT FOUND" indicates a wiring issue that needs to be fixed before continuing.
- Calibration: When prompted, sit upright and remain still for the 3-second countdown, then hold that position while the system averages 100 samples to set the upright reference angle.
-
Angle threshold tuning: Slouch forward and confirm the LED turns on once
angleDifferenceexceedsSLOUCH_THRESHOLDin the Serial Monitor output. Adjust the threshold if the alert triggers too early or too late for your seating posture. -
Stillness and alert timing: Slouch continuously without moving and confirm the buzzer and motor activate once
slouchElapsedreachesSLOUCH_LIMITandstillnessCounterreachesSTILLNESS_COUNT_THRESHOLD. Then slouch while shifting position frequently and confirm the alert does not trigger, since the stillness condition isn't met. - Recovery check: Return to an upright position and confirm the buzzer and motor stop immediately, and that the slouch timer resets correctly on the next slouch.
09 System Demonstration
Circuit Demonstration
Product Demonstration
10 Conclusion
The completed system reliably detects sustained slouching and alerts the wearer through both audible and haptic feedback, while avoiding false triggers during normal movement by requiring a period of stillness alongside the angle threshold. This setup is intended for everyday desk use to build better posture habits over time.
Possible Improvements:
- Add a gyroscope-based filter (e.g. a complementary or Kalman filter combining the MPU6050's accelerometer and gyroscope) for more stable angle readings during small movements.
- Add a mobile app or Bluetooth/WiFi logging so posture data can be tracked over the course of a day or week.
- Allow the slouch angle threshold and time limit to be adjusted without reflashing the firmware, for example via a button-based menu or companion app.
- Replace the continuous buzzer tone with a pulsing pattern for a gentler, less startling alert.
11 References
-
Seeed Studio
wiki.seeedstudio.com (XIAO SAMD21 documentation) -
InvenSense
MPU6050 datasheet and register map -
Arduino
docs.arduino.cc (Wire library reference)
12 Project Author
- Lukie-Mar Lonzaga
- Miles Darren Bagnol
- Elijah Joseph Orias
