Contents β 25 sections
Project Overview
A small upper-back sensor that learns your good posture, notices sustained slouching rather than momentary movement, and nudges you with a brief vibration instead of an alarm.
Posture wearables fail for a predictable reason: they nag. A device that buzzes every time you lean forward to pick something up gets removed within a day. The engineering problem is not detecting a slouch β a single accelerometer does that trivially β it is deciding which slouches are worth interrupting someone about.
This design makes three choices that address that directly. It measures sustained deviation, requiring a poor posture to persist for a configurable period (default 45 seconds) before responding, so reaching, bending and stretching are ignored entirely. It uses a calibrated personal reference rather than an absolute angle, because "upright" differs between people and between a chair and a standing desk. And it responds with one brief haptic pulse, escalating only if ignored, rather than a continuous alert.
The sensing is deliberately simple. An accelerometer worn on the upper back measures the gravity vector, and the angle between that vector and a stored reference gives forward flexion directly. No gyroscope, no integration, no drift. The trade-off is that it cannot distinguish leaning back from leaning forward without additional axes handling, and it cannot measure rotation β but for the thoracic flexion that constitutes most desk slouching, one accelerometer is genuinely sufficient.
The device also logs, which is where most of the actual value is. A week of posture data showing that your slouch angle degrades steadily after 40 minutes of sitting is far more actionable than a buzz β it tells you to set a timer, not to try harder.
What this project does
- Measures upper-back flexion angle from the gravity vector, referenced to a calibrated upright posture.
- Requires sustained poor posture before responding, so normal movement is ignored.
- Delivers a single brief haptic pulse, escalating only if the posture does not correct.
- Detects and adapts to different contexts β sitting, standing, walking β using motion variance.
- Logs posture angle continuously and produces a daily summary of time in good posture.
- Runs for a week on a small cell through duty-cycled sampling.
- Recalibrates on demand with a single button press.
Real-World Applications
| Setting | How it is used |
|---|---|
| Desk work and study | The main case β sustained seated flexion over hours, which is exactly what the sustained-deviation logic targets. |
| Physiotherapy adherence | A patient told to maintain neutral spine benefits enormously from objective feedback between appointments. |
| Musicians and dentists | Occupations with sustained asymmetric or flexed postures and high rates of related musculoskeletal problems. |
| Manual handling training | Detecting a bent-back lift in real time is a genuinely useful safety intervention. |
| Post-injury rehabilitation | Objective data on whether protective posture is resolving over weeks. |
| Ergonomics assessment | A week of logged data justifies a desk change far better than a subjective complaint. |
Deployment contexts where a build of this kind earns its keep.
Features & Capabilities
- Personal calibration β the reference is your upright, not a fixed angle.
- Sustained-deviation logic with a 45-second default, tunable per user.
- Context detection from accelerometer variance: no nagging while walking.
- Escalating haptics β one pulse, then two, then three, then silence for ten minutes.
- Adaptive reference drift so a slowly changing chair or desk setup does not require recalibration.
- Daily posture score as percentage of monitored time within threshold.
- BLE sync to a phone for the log, with local buffering when out of range.
- Silent mode for meetings, triggered by a double button press.
Difficulty, Time & Required Skills
| Attribute | Value |
|---|---|
| Difficulty level | Beginner |
| Estimated completion time | 6β10 hours |
| Indicative build cost | βΉ1,600 β βΉ2,300 |
| Primary discipline | Health & Wearables |
| Reference platform | ESP32 DevKit V1 (ESP-WROOM-32) |
Skills you should have (or will pick up)
- Arduino C++ with basic vector maths
- IΒ²C sensor reading
- Understanding of the gravity vector and dot products
- Simple state machines and timers
- BLE basics for the sync half
Bill of Materials
Every part below is commonly available from Indian and international hobby-electronics suppliers. Prices are indicative 2026 retail figures in Indian rupees and will drift β treat them as a budgeting guide, not a quotation.
| Component | Key specification | Qty | Approx. cost |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) Wi-Fi transmit bursts peak near 500 mA β size the regulator accordingly. | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18Γ 12-bit ADC, 2Γ 8-bit DAC | 1 | βΉ450 |
| MPU-6050 6-axis IMU Gyro bias drifts with temperature β re-zero at boot while the device is still. | 3-axis gyro Β±250β2000 Β°/s, 3-axis accel Β±2β16 g, 16-bit ADC, on-chip DMP | 1 | βΉ190 |
| 18650 Li-ion cell 3400 mAh + holder Never charge below 0 Β°C; always use a protected cell or a BMS. | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 1 | βΉ450 |
| TP4056 Li-ion charger + DW01 protection Buy the version *with* protection ICs β the bare charger will over-discharge your cell. | 1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection | 1 | βΉ45 |
| Double-sided perfboard 7 Γ 9 cm + headers Solder female headers so the MCU can be swapped without desoldering. | FR-4, 0.1β³ pitch, plated through-holes, 24 Γ 18 grid | 1 | βΉ60 |
| Coin vibration motor + driver | 10 mm, 3 V, 80 mA, with flyback diode | 1 | βΉ120 |
| Tactile button | 6 mm, NO | 1 | βΉ20 |
| 3D-printed clip housing Must attach rigidly to clothing or skin β a housing that shifts invalidates the calibration. | TPU, adhesive or clip mount | 1 | βΉ150 |
| 400 mAh LiPo cell | 3.7 V, protected | 1 | βΉ220 |
| Medical-grade adhesive pads Skin mounting gives far better data than clipping to a shirt. | Hypoallergenic, 40 mm | 1 | βΉ180 |
Estimated total: βΉ1,885, excluding tools, shipping and consumables.
Tools and consumables
- Soldering iron (temperature controlled, 350 Β°C) with 0.8 mm 60/40 or lead-free solder
- Digital multimeter β continuity, DC volts and current ranges
- Wire strippers, flush cutters and a small set of precision screwdrivers
- Heat-shrink tubing and a heat gun (or a lighter, carefully)
- A laptop with a USB port and the toolchain listed above
Hardware Specifications
| Part | Specification | Supply | Interface | Reference |
|---|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | Dual-core Xtensa LX6 @ 240 MHz, 520 KB SRAM, 4 MB flash, Wi-Fi 802.11 b/g/n + BLE 4.2, 34 GPIO, 18Γ 12-bit ADC, 2Γ 8-bit DAC | 3.3 V logic / 5 V USB | UART, SPI, IΒ²C, IΒ²S, CAN, PWM | Datasheet |
| MPU-6050 6-axis IMU | 3-axis gyro Β±250β2000 Β°/s, 3-axis accel Β±2β16 g, 16-bit ADC, on-chip DMP | 2.375β3.46 V (module 5 V tolerant) | IΒ²C (0x68/0x69) | Datasheet |
| 18650 Li-ion cell 3400 mAh + holder | 3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge | 3.0β4.2 V | Holder / spot-welded tabs | Datasheet |
| TP4056 Li-ion charger + DW01 protection | 1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection | 4.5β5.5 V in | micro-USB / pads | Datasheet |
| Double-sided perfboard 7 Γ 9 cm + headers | FR-4, 0.1β³ pitch, plated through-holes, 24 Γ 18 grid | β | β | Datasheet |
Consolidated electrical and interface specifications for every active part in the build.
Power Budget & Supply Sizing
Add up the typical active current of every part, then size the supply with at least 50 % headroom so transmit bursts and motor inrush never brown out the controller.
| Load | Supply rail | Typical current (mA) | Notes |
|---|---|---|---|
| ESP32 DevKit V1 (ESP-WROOM-32) | 3.3 V logic / 5 V USB | 160 | Wi-Fi transmit bursts peak near 500 mA β size the regulator accordingly. |
| MPU-6050 6-axis IMU | 2.375β3.46 V (module 5 V tolerant) | 3.9 | Gyro bias drifts with temperature β re-zero at boot while the device is still. |
| TP4056 Li-ion charger + DW01 protection | 4.5β5.5 V in | 1000 | Buy the version *with* protection ICs β the bare charger will over-discharge your cell. |
Summed typical draw is 1163.9 mA. With a 1.5Γ design margin the supply should deliver at least 1800 mA continuously at the stated rail voltage.
Software Requirements & Development Environment
Reference toolchain: Arduino IDE 2.3.x with the ESP32 board package 3.x (or PlatformIO on VS Code). Anything newer normally works; anything older may lack the board definitions used here.
- Install the Arduino IDE 2.3.x (or PlatformIO if you prefer a real editor and dependency locking).
- Add
https://espressif.github.io/arduino-esp32/package_esp32_index.jsonunder File β Preferences β Additional Board Manager URLs, then install esp32 from the Boards Manager. - Set the correct port under Tools β Port. On Linux add yourself to the
dialoutgroup:sudo usermod -aG dialout $USERand log out and back in. - Open the Serial Monitor at 115200 baud β every sketch here logs its state there.
- Keep File β Preferences β Show verbose output during: compilation switched on while you are debugging build errors.
Required libraries
| Library | Why it is needed | Install |
|---|---|---|
| MPU6050_light / Adafruit MPU6050 1.3.x | IMU register access, calibration and complementary-filter angles. | Library Manager β "MPU6050_light" by rfetick |
| Preferences (NVS) bundled | Wear-levelled key/value storage in ESP32 flash for settings. | Bundled with the ESP32 core |
Block Diagram
The block diagram shows the functional decomposition of the system β what senses, what decides, what acts, and where the data ends up.
Circuit Diagram & Wiring
Every signal line in the build is shown below, followed by a pin-by-pin connection table you can work through with a multimeter in hand.
| Peripheral | Peripheral pin | Controller pin | Signal |
|---|---|---|---|
| MPU-6050 IMU | SDA / SCL | GPIO 21 / 22 | IΒ²C at 0x68 |
| MPU-6050 INT | INT | GPIO 33 | Motion wake, RTC-capable |
| Button | NO | GPIO 32 | Calibrate / silent mode |
| Battery divider | Mid-point | GPIO 34 | 1 MΞ© / 1 MΞ©, gated |
| Vibration motor | Transistor base | GPIO 25 | Through 1 kΞ© + flyback |
| Status LED | Anode | GPIO 26 | Brief flashes only |
Wire one row at a time and tick it off β most "it does not work" reports trace back to a single swapped pair.
Wiring explanation
- The sensor must be rigidly attached to the body, not loosely to clothing. A device clipped to a loose shirt measures the shirt, not the spine, and the calibration becomes meaningless within minutes.
- Mount at approximately T3βT5 β upper thoracic, between the shoulder blades. Lower down measures lumbar flexion, which is a different (and also valid) measurement requiring a different reference.
- Orient the board consistently. The firmware assumes a particular axis points up when upright; rotating the housing 90Β° between sessions invalidates everything.
- The vibration motor needs a transistor and a flyback diode. It draws about 80 mA, well beyond a GPIO, and its back-EMF will damage the pin without the diode.
- Keep the assembly small and light. A device that is uncomfortable is a device that is not worn, and the whole project value depends on it being worn all day.
- Use a bare ESP32 module for the deployed build. A development board's sleep current makes week-long battery life impossible.
System Architecture
Read the stack from the bottom up: physical hardware, the firmware that drives it, the transport that moves data off the device, and the software a human actually looks at.
Working Principle
An accelerometer at rest measures the reaction to gravity, so its output is a vector pointing "up" in the device's own frame. If the device is rigidly attached to the upper back, that vector rotates exactly as the thoracic spine flexes. The angle between the current gravity vector and a stored reference vector is therefore the flexion angle directly, with no integration and no drift β which is why a gyroscope is not needed here.
The calculation is a dot product. Normalise both vectors, take their dot product, and the arccosine gives the angle between them. This is robust in a way that Euler angles are not: there is no gimbal lock, no axis ordering convention to get wrong, and no discontinuity at any orientation.
The reference is the important part. There is no universal "good posture" angle β it depends on body proportions, the chair, the desk height, and what the person is doing. Calibrating against the individual's own comfortable upright, held for five seconds at the press of a button, produces a threshold that means something. An absolute angle threshold produces a device that is wrong for most people.
Sustained deviation is what makes it tolerable to wear. Reaching for a mug, bending to pick something up, leaning to talk to someone β all produce large flexion angles for a few seconds, and none of them are the problem. Requiring the deviation to persist for 45 seconds ignores essentially all of them while still catching the slow slump into a screen, which is the posture that actually causes trouble. Tuning that single parameter is the difference between a device that is worn and one that is not.
Context detection from acceleration variance adds a second filter. Walking produces high variance; sitting still produces almost none. Suppressing alerts while the variance is high avoids nagging someone who is moving around, and it also avoids false readings, because the gravity-vector measurement is only valid when dynamic acceleration is small compared with 1 g.
Finally, the reference drift. Over weeks, a person's chair, desk and habitual posture change slowly. A reference that never updates gradually becomes wrong. Updating it very slowly β a time constant of hours, and only during periods classified as good posture β keeps it aligned with the person's actual neutral without letting a bad day drag it downward.
The maths behind it
Flexion angle from gravity vectors
Reference (calibrated upright): r = (rx, ry, rz), normalised
Current: a = (ax, ay, az), normalised
cos ΞΈ = a Β· r = axΒ·rx + ayΒ·ry + azΒ·rz
ΞΈ = acos(clamp(cos ΞΈ, β1, 1)) Γ 180/Ο
Worked example:
r = (0.05, 0.08, 0.995)
a = (0.05, 0.42, 0.906)
aΒ·r = 0.0025 + 0.0336 + 0.9015 = 0.9376
ΞΈ = acos(0.9376) = 20.4Β°
Typical thresholds: 15Β° mild slouch, 25Β° pronounced.
Context from acceleration variance
Over a 2-second window at 10 Hz (20 samples):
Ο = sqrt( (1/N) Β· Ξ£ (|a|α΅’ β mean)Β² )
Measured on the reference build:
sitting still : Ο β 0.01β0.03 g
typing : Ο β 0.03β0.06 g
standing still : Ο β 0.02β0.05 g
walking : Ο β 0.25β0.60 g
running : Ο > 0.8 g
Gate alerts when Ο > 0.15 g β comfortably above
typing and below walking.
Battery life with duty cycling
MPU-6050 active : 3.9 mA
ESP32 light sleep : 0.8 mA
ESP32 active (10 % duty): 80 mA Γ 0.10 = 8 mA
Vibration (20 pulses/day, 400 ms): negligible average
Average with 10 Hz sampling in light sleep β 5.5 mA
400 mAh cell, usable 340 mAh:
340 / 5.5 = 62 h β 2.6 days
With motion-gated deep sleep at night (8 h at 15 Β΅A):
daily consumption drops ~33 % β about 4 days.
Sampling at 4 Hz instead of 10 Hz gets to a week.
Program Flowchart
The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.
Assembly Instructions
Build on a breadboard first and only commit to solder once the whole system has run for an hour without a fault.
Step-by-Step Implementation Guide
Work through these in order. Each step ends in something you can observe, so a failure is always localised to the step you just finished.
Complete Source Code
The listing below is complete and compiles as written β there are no elided sections. Read the annotations under each block before you upload it.
/* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Posture Correction Wearable β ESP32 + MPU-6050
Measures upper-back flexion as the angle between the current
gravity vector and a personally calibrated upright reference.
Responds only to SUSTAINED deviation, and never while moving.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
#include <Wire.h>
#include <Preferences.h>
#include <math.h>
#define MPU_ADDR 0x68
#define PIN_INT 33
#define PIN_BTN 32
#define PIN_VIBE 25
#define PIN_LED 26
#define PIN_BATT 34
#define PIN_BATT_EN 14
#define SAMPLE_HZ 10
#define VAR_WINDOW 20 // 2 s at 10 Hz
#define MOVING_SIGMA 0.15f // g
#define ANGLE_THRESHOLD 18.0f // degrees from reference
#define SUSTAIN_MS 45000UL
#define ESCALATE_MS 60000UL
#define QUIET_AFTER_MS 600000UL // 10 min silence after 3 escalations
#define REF_DRIFT_ALPHA 0.00005f // ~hours time constant
Preferences prefs;
float refX = 0, refY = 0, refZ = 1;
float angleNow = 0, sigma = 0;
uint32_t badSince = 0, lastNudge = 0, quietUntil = 0;
uint8_t escalation = 0;
bool silentMode = false;
uint32_t goodSeconds = 0, monitoredSeconds = 0;
float magHist[VAR_WINDOW];
uint8_t magIdx = 0;
/* ββ MPU-6050 βββββββββββββββββββββββββββββββββββββββββββββββββ */
void mpuWrite(uint8_t reg, uint8_t val) {
Wire.beginTransmission(MPU_ADDR);
Wire.write(reg); Wire.write(val);
Wire.endTransmission();
}
void mpuBegin() {
mpuWrite(0x6B, 0x00); // wake from sleep
mpuWrite(0x1C, 0x00); // accel range +/-2 g, best resolution
mpuWrite(0x1A, 0x05); // DLPF 10 Hz β we only want gravity
mpuWrite(0x19, 0x63); // sample rate divider -> 10 Hz
}
void mpuReadAccel(float &x, float &y, float &z) {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B);
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADDR, 6);
int16_t rx = (Wire.read() << 8) | Wire.read();
int16_t ry = (Wire.read() << 8) | Wire.read();
int16_t rz = (Wire.read() << 8) | Wire.read();
x = rx / 16384.0f; y = ry / 16384.0f; z = rz / 16384.0f; // +/-2 g
}
/* ββ geometry βββββββββββββββββββββββββββββββββββββββββββββββββ */
float flexionAngle(float x, float y, float z) {
float n = sqrtf(x * x + y * y + z * z);
if (n < 0.1f) return 0;
float dot = (x * refX + y * refY + z * refZ) / n;
dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot);
return acosf(dot) * 180.0f / (float)M_PI;
}
void calibrate() {
digitalWrite(PIN_LED, HIGH);
for (int i = 0; i < 3; i++) { digitalWrite(PIN_VIBE, HIGH); delay(120);
digitalWrite(PIN_VIBE, LOW); delay(180); }
delay(1500); // let the user settle into upright
float sx = 0, sy = 0, sz = 0;
for (int i = 0; i < 50; i++) { // 5 s of averaging
float x, y, z; mpuReadAccel(x, y, z);
sx += x; sy += y; sz += z;
delay(100);
}
float n = sqrtf(sx * sx + sy * sy + sz * sz);
refX = sx / n; refY = sy / n; refZ = sz / n;
prefs.putFloat("rx", refX);
prefs.putFloat("ry", refY);
prefs.putFloat("rz", refZ);
digitalWrite(PIN_VIBE, HIGH); delay(400); digitalWrite(PIN_VIBE, LOW);
digitalWrite(PIN_LED, LOW);
Serial.printf("Calibrated: (%.3f, %.3f, %.3f)\n", refX, refY, refZ);
}
/* ββ context ββββββββββββββββββββββββββββββββββββββββββββββββββ */
void updateVariance(float mag) {
magHist[magIdx] = mag;
magIdx = (magIdx + 1) % VAR_WINDOW;
float mean = 0;
for (float v : magHist) mean += v;
mean /= VAR_WINDOW;
float var = 0;
for (float v : magHist) { float d = v - mean; var += d * d; }
sigma = sqrtf(var / VAR_WINDOW);
}
bool isMoving() { return sigma > MOVING_SIGMA; }
/* ββ haptics ββββββββββββββββββββββββββββββββββββββββββββββββββ */
void nudge(uint8_t pulses) {
if (silentMode) return;
for (uint8_t i = 0; i < pulses; i++) {
digitalWrite(PIN_VIBE, HIGH);
delay(180);
digitalWrite(PIN_VIBE, LOW);
delay(160);
}
}
/* ββ setup / loop βββββββββββββββββββββββββββββββββββββββββββββ */
void setup() {
Serial.begin(115200);
pinMode(PIN_VIBE, OUTPUT); pinMode(PIN_LED, OUTPUT);
pinMode(PIN_BTN, INPUT_PULLUP);
Wire.begin(21, 22);
mpuBegin();
prefs.begin("posture", false);
refX = prefs.getFloat("rx", 0);
refY = prefs.getFloat("ry", 0);
refZ = prefs.getFloat("rz", 1);
for (float &v : magHist) v = 1.0f;
Serial.println("Posture wearable running β hold the button to calibrate");
}
void loop() {
static uint32_t lastSample = 0;
if (millis() - lastSample < 1000 / SAMPLE_HZ) return;
lastSample = millis();
float x, y, z;
mpuReadAccel(x, y, z);
float mag = sqrtf(x * x + y * y + z * z);
updateVariance(mag);
angleNow = flexionAngle(x, y, z);
/* Button: short press = silent mode, long press = calibrate. */
if (digitalRead(PIN_BTN) == LOW) {
uint32_t held = millis();
while (digitalRead(PIN_BTN) == LOW && millis() - held < 3000) delay(20);
if (millis() - held >= 2000) calibrate();
else {
silentMode = !silentMode;
digitalWrite(PIN_LED, silentMode);
nudge(silentMode ? 1 : 2);
}
badSince = 0; escalation = 0;
return;
}
/* Only evaluate posture when the body is reasonably still. The
gravity-vector measurement is only valid when dynamic
acceleration is small, and nobody wants a buzz while walking. */
if (isMoving()) {
badSince = 0;
escalation = 0;
return;
}
monitoredSeconds++;
if (angleNow < ANGLE_THRESHOLD) {
goodSeconds++;
badSince = 0;
escalation = 0;
// Slowly pull the reference towards sustained good posture, so a
// gradual change of chair or desk does not require recalibration.
float n = sqrtf(x * x + y * y + z * z);
refX = (1 - REF_DRIFT_ALPHA) * refX + REF_DRIFT_ALPHA * (x / n);
refY = (1 - REF_DRIFT_ALPHA) * refY + REF_DRIFT_ALPHA * (y / n);
refZ = (1 - REF_DRIFT_ALPHA) * refZ + REF_DRIFT_ALPHA * (z / n);
return;
}
/* Poor posture β but only act once it is SUSTAINED. */
if (!badSince) { badSince = millis(); return; }
if (millis() < quietUntil) return;
uint32_t sustained = millis() - badSince;
if (sustained < SUSTAIN_MS) return;
if (millis() - lastNudge < ESCALATE_MS) return;
lastNudge = millis();
escalation++;
nudge(escalation);
Serial.printf("nudge %u β angle %.1f deg sustained %lus\n",
escalation, angleNow, sustained / 1000);
if (escalation >= 3) {
// Three ignored nudges: the user is not going to respond right now.
// Going quiet for ten minutes is what stops the device being removed.
quietUntil = millis() + QUIET_AFTER_MS;
escalation = 0;
badSince = 0;
}
static uint32_t lastReport = 0;
if (millis() - lastReport > 60000) {
lastReport = millis();
Serial.printf("angle %.1f sigma %.3f good %lu/%lu s (%.0f%%)\n",
angleNow, sigma, goodSeconds, monitoredSeconds,
monitoredSeconds ? goodSeconds * 100.0f / monitoredSeconds : 100.0f);
}
}
Configuration & Calibration
Configuration steps
- Set
ANGLE_THRESHOLDfrom your own data. 18Β° is a reasonable default; a physiotherapist may recommend tighter for a specific condition. - Set
SUSTAIN_MS. This is the parameter that determines whether the device is worn. Start at 45 seconds and increase if it feels intrusive β a device set to 5 seconds will be in a drawer by Tuesday. - Tune
MOVING_SIGMAfrom your own variance measurements while typing and while walking. It must sit clearly between the two. - Set
REF_DRIFT_ALPHAconservatively. Too fast and the reference follows your slouch; 0.00005 at 10 Hz gives a time constant of about half an hour of good posture. - Recalibrate whenever the mounting position changes, and at the start of each session if you clip rather than adhere the device.
Calibration procedure
An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.
Calibrate the upright reference
Sit or stand in the posture a physiotherapist would call neutral β ears over shoulders, shoulders over hips β and hold it for the five-second averaging window. This is the reference everything else is measured against, so it is worth taking seriously rather than doing it slumped.
Measure your own variance thresholds
Log sigma while typing, while sitting still, and while walking. The gate must be clearly above your typing value and clearly below your walking value. Mine were 0.04 and 0.35; yours will differ with mounting and body mass.
Find your sustain time
Wear it for a day at 45 seconds. Count the nudges. Fewer than five a day is probably too permissive; more than twenty is intrusive. Adjust and repeat β this is the parameter that determines whether the device gets worn.
Verify the angle against a photograph
Have someone photograph you from the side in good and poor posture and measure the actual thoracic angle change. Compare against what the device reports. Agreement within about 5Β° confirms the mounting is rigid enough.
Testing Procedure & Expected Output
Test from the bottom up. Confirm power, then each sensor in isolation, then the integrated loop β the first failing step tells you exactly where to look.
| Test | What you should see |
|---|---|
| Calibrate while sitting upright | Three short pulses, five seconds of stillness, one long confirmation pulse, and an angle reading near 0Β°. |
| Slouch forward deliberately | Angle rises to 20β35Β°. No immediate response β the sustain timer must run first. |
| Hold the slouch for 50 seconds | A single haptic pulse at about 45 seconds. |
| Keep slouching | Two pulses at 105 s, three at 165 s, then ten minutes of silence. |
| Correct posture | Timer resets immediately, no further nudges. |
| Reach forward for three seconds and return | No response at all β this is the false positive the sustain timer exists to prevent. |
| Walk around | Sigma exceeds the threshold, monitoring is suspended, and no nudges occur regardless of angle. |
| Short press the button | One pulse and the LED lights β silent mode. Press again for two pulses and normal operation. |
Bench-test checklist. If a row fails, stop and fix it before moving on.
Expected output
With everything wired and the firmware uploaded, the Serial Monitor at 115200 baud should look similar to the trace below. Values will differ; the shape of the output should not.
Troubleshooting: Common Errors & Fixes
Performance Optimisation
- Use the MPU-6050 hardware low-pass filter rather than filtering in software β it removes noise before quantisation, which software filtering cannot.
- Sample at 4β10 Hz. Posture changes over seconds; sampling faster costs battery and gains nothing.
- Use the motion interrupt to deep-sleep overnight. Eight hours of not being worn is a third of the day and currently costs full running power.
- Replace every
delay()with amillis()comparison β blocking delays are the single most common cause of dropped readings. - Sample sensors on a fixed cadence and publish on a slower one; you almost never need to transmit at the sampling rate.
- Move networking into its own FreeRTOS task so a slow DNS lookup cannot stall the control loop.
- Use
uint8_t/uint16_twhere the range allows; on an 8-bit AVR a 32-bit add costs four times as much. - For battery builds use deep sleep between samples: an ESP32 drops from ~160 mA awake to about 10 Β΅A asleep, which is the difference between days and months of runtime.
- Profile before optimising β print
micros()deltas around each stage and fix the slowest one first.
Safety Precautions
- This is a feedback aid, not a treatment. Persistent back or neck pain needs a clinician, and a wearable that makes you tense up trying to hold a position can make things worse rather than better.
- Adhesive mounting can irritate skin over days. Use hypoallergenic pads, rotate the position slightly, and stop if irritation appears.
- Do not chase a perfect score. Sustained rigid "good" posture is itself a problem β movement variety matters more than any single angle, and a device that encourages statue-like stillness is optimising the wrong thing.
- Lithium cells vent and burn when abused. Only use protected cells or a proper BMS, never charge below 0 Β°C, and never leave a charging pack unattended on a wooden desk.
- Wear eye protection when soldering or cutting, and solder in a ventilated space β rosin flux fumes are a respiratory irritant.
- Power the circuit through a bench supply with a current limit while you are testing. A 300 mA limit turns a wiring mistake into a beep instead of a dead board.
- Disconnect power before changing any wiring. Hot-plugging a sensor onto a live bus is the fastest way to lose a controller.
Maintenance
- Re-check every screw terminal and header after the first week β thermal cycling loosens connections that felt tight on day one.
- Log pack voltage. When resting voltage after a full charge drops below about 4.0 V, the cell is near end of life β replace it.
- Recalibrate at the interval given in the calibration section, and keep the constants in a text file next to the firmware β not only in flash.
- Keep a short logbook of firmware versions and what changed. Six months later you will not remember why that constant is 1.083.
Future Improvements & Upgrades
A working v1 is a platform, not a finish line. These are the upgrades that add the most capability for the least rework.
- Add a second sensor at the lumbar spine to measure the relative angle between thoracic and lumbar segments, which is far more informative than either alone.
- Add BLE sync to a phone app with a daily posture timeline, which is where most of the actual behavioural value lies.
- Add break prompting β the data almost always shows posture degrading after a certain sitting duration, and prompting a stand at that point is more effective than correcting the slouch.
- Add a small trained classifier to distinguish sitting, standing, walking and lying rather than a single variance threshold.
- Add shoulder protraction detection with a magnetometer or a second sensor, since rounded shoulders often matter more than thoracic flexion for desk workers.
- Design a proper PCB. Once the breadboard version has run for a month, moving to a two-layer board removes the intermittent-contact failures that dominate prototype faults.
- Add connectivity β an ESP32 and an MQTT publish turn a local gadget into something you can graph, alert on and analyse over months.
- Add persistent local storage (microSD or the on-chip flash) so a network outage does not create a hole in your data.
- Move configuration out of the source: a captive-portal setup page or a JSON config file makes the build reusable without a recompile.
- Add a battery and solar option so the unit survives a power cut and can be sited away from a socket.
- Write a small test harness that feeds synthetic sensor values through the decision logic, so you can validate thresholds without physically triggering the event.
Frequently Asked Questions
References & Learning Resources
These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.
- MPU-6000/6050 six-axis motion tracking device β datasheetTDK InvenSense
- O'Sullivan et al., "Neutral lumbar spine sitting posture in pain-free subjects"Manual Therapy, 2010
- Simpson et al., "The effect of wearable posture devices on posture and pain: a systematic review"Applied Ergonomics, 2019
- Accelerometer-based inclinometry β theory and limitationsAnalog Devices
- ISO 11226 β Ergonomics: evaluation of static working posturesISO