Siddhant Kumar
Project 018 Β· Health & Wearables

Posture Correction Wearable.

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.

Beginner 6–10 hours 33 min read IMUHapticsWearable
Jump to source Bill of materials
Posture Correction Wearable β€” reference build illustration MCU VCC Β· GND Β· SIG Β· NC
Difficulty
Beginner
Build time
6–10 hours
Indicative cost
β‚Ή1,600 – β‚Ή2,300
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Health & Wearables
Last updated
28 July 2026
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.

A wrist-worn fitness tracker
A body-worn sensor. Rigid attachment matters more than sensor quality here β€” a device clipped to a loose shirt measures the shirt. Photograph sourced from Wikimedia Commons β€” Fitness tracker.jpg. Reused under the licence stated on that page; please check it before republishing.

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

SettingHow it is used
Desk work and studyThe main case β€” sustained seated flexion over hours, which is exactly what the sustained-deviation logic targets.
Physiotherapy adherenceA patient told to maintain neutral spine benefits enormously from objective feedback between appointments.
Musicians and dentistsOccupations with sustained asymmetric or flexed postures and high rates of related musculoskeletal problems.
Manual handling trainingDetecting a bent-back lift in real time is a genuinely useful safety intervention.
Post-injury rehabilitationObjective data on whether protective posture is resolving over weeks.
Ergonomics assessmentA 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

AttributeValue
Difficulty levelBeginner
Estimated completion time6–10 hours
Indicative build costβ‚Ή1,600 – β‚Ή2,300
Primary disciplineHealth & Wearables
Reference platformESP32 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.

ComponentKey specificationQtyApprox. 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 DAC1β‚Ή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 DMP1β‚Ή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 discharge1β‚Ή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 protection1β‚Ή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 grid1β‚Ή60
Coin vibration motor + driver10 mm, 3 V, 80 mA, with flyback diode1β‚Ή120
Tactile button6 mm, NO1β‚Ή20
3D-printed clip housing
Must attach rigidly to clothing or skin β€” a housing that shifts invalidates the calibration.
TPU, adhesive or clip mount1β‚Ή150
400 mAh LiPo cell3.7 V, protected1β‚Ή220
Medical-grade adhesive pads
Skin mounting gives far better data than clipping to a shirt.
Hypoallergenic, 40 mm1β‚Ή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

PartSpecificationSupplyInterfaceReference
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 DAC3.3 V logic / 5 V USBUART, SPI, IΒ²C, IΒ²S, CAN, PWMDatasheet
MPU-6050 6-axis IMU3-axis gyro Β±250–2000 Β°/s, 3-axis accel Β±2–16 g, 16-bit ADC, on-chip DMP2.375–3.46 V (module 5 V tolerant)IΒ²C (0x68/0x69)Datasheet
18650 Li-ion cell 3400 mAh + holder3.7 V nominal, 4.2 V full, 3400 mAh, ~12.6 Wh, 2 C discharge3.0–4.2 VHolder / spot-welded tabsDatasheet
TP4056 Li-ion charger + DW01 protection1 A programmable CC/CV charge to 4.2 V Β±1 %, over-discharge and short protection4.5–5.5 V inmicro-USB / padsDatasheet
Double-sided perfboard 7 Γ— 9 cm + headersFR-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.

LoadSupply railTypical current (mA)Notes
ESP32 DevKit V1 (ESP-WROOM-32)3.3 V logic / 5 V USB160Wi-Fi transmit bursts peak near 500 mA β€” size the regulator accordingly.
MPU-6050 6-axis IMU2.375–3.46 V (module 5 V tolerant)3.9Gyro bias drifts with temperature β€” re-zero at boot while the device is still.
TP4056 Li-ion charger + DW01 protection4.5–5.5 V in1000Buy 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.json under 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 dialout group: sudo usermod -aG dialout $USER and 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

LibraryWhy it is neededInstall
MPU6050_light / Adafruit MPU6050 1.3.xIMU register access, calibration and complementary-filter angles.Library Manager β†’ "MPU6050_light" by rfetick
Preferences (NVS) bundledWear-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.

Posture Correction Wearable β€” system block diagramFunctional block diagram of the Posture Correction Wearable system. SenseMPU-6050gravity vectorMotion variancecontextReferenceCalibrated uprightper userSlow drift trackchair changesDecideSustained > 45 s?ignore transientsContext gatenot while walkingRespondEscalating haptic1 β†’ 2 β†’ 3 pulsesLog + daily scorethe real valueaccel XYZflexion angleconfirmed slouch
Posture Correction Wearable β€” system block diagram

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.

Posture Correction Wearable β€” wiring schematicConnection schematic showing which controller pin drives each peripheral. Sensors / InputsControllerActuators / OutputsESP32 DevKit V1(ESP-WROOM-32)3.3 V logic / 5 V USBMPU-6050 IMUGPIO 21 / 22IΒ²C at 0x68MPU-6050 INTGPIO 33Motion wake,RTC-capableButtonGPIO 32Calibrate / silentmodeBattery dividerGPIO 341 MΞ© / 1 MΞ©, gatedVibration motorGPIO 25Through 1 kΞ© +flybackStatus LEDGPIO 26Brief flashes only
Posture Correction Wearable β€” wiring schematic
PeripheralPeripheral pinController pinSignal
MPU-6050 IMUSDA / SCLGPIO 21 / 22IΒ²C at 0x68
MPU-6050 INTINTGPIO 33Motion wake, RTC-capable
ButtonNOGPIO 32Calibrate / silent mode
Battery dividerMid-pointGPIO 341 MΞ© / 1 MΞ©, gated
Vibration motorTransistor baseGPIO 25Through 1 kΞ© + flyback
Status LEDAnodeGPIO 26Brief 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.

Posture Correction Wearable β€” architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) Β· MPU-6050 6-axis IMUDriver layermpu Β· preferencesApplication logicsampling loop Β· filtering Β· thresholds Β· state machinePresentation layerlocal display Β· serial console Β· logged output
Posture Correction Wearable β€” architecture stack

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

plainFlexion 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

plainContext 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

plainBattery 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.

Posture Correction Wearable β€” firmware flowchartControl flow through the main program loop. Boot: load calibrationreferenceSample accelerometer at 10 HzCompute flexion angle vsreferenceMoving (variance high)?skip, reset timerevaluateAngle beyond threshold?start/continue timerreset timerSustained past 45 s?haptic nudgekeep countingEscalate if still bad after 60sLog angle, update daily score
Posture Correction Wearable β€” firmware flowchart

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.

cppposture-wearable.ino
/* ═══════════════════════════════════════════════════════════════
   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);
  }
}
mpuWrite(0x1A, 0x05) DLPF at 10 HzThe digital low-pass filter removes dynamic acceleration above 10 Hz before the data reaches the registers. Since only the gravity vector matters here, filtering hard in hardware is free noise reduction.
flexionAngle via dot productNo Euler angles, no gimbal lock, no axis-order convention to get wrong. The angle between two vectors is a single well-defined number at every orientation, which makes this the most robust formulation available.
isMoving() gate before evaluatingTwo purposes at once: it stops the device nagging someone who is walking, and it ensures the gravity-vector measurement is only used when dynamic acceleration is small enough for it to be valid.
badSince timer, not instantaneous thresholdThis is the single most important design decision. Reaching for something produces a 40Β° flexion for three seconds; a slump into a screen produces 25Β° for twenty minutes. Only the second one is worth interrupting someone about.
REF_DRIFT_ALPHA applied only during good postureThe reference tracks the person's actual neutral over hours. Updating it during poor posture would let a bad day drag the reference downward until slouching becomes the new normal β€” which is exactly the failure mode to avoid.
quietUntil after three ignored nudgesIf someone has ignored three escalating nudges they are concentrating on something. Continuing to buzz gets the device taken off, which ends all its value. Ten minutes of silence is what makes it tolerable to wear all day.
Long press calibrates, short press silencesThe two functions a user actually needs, on one button, distinguishable by feel without looking β€” which matters for a device worn between the shoulder blades.

Configuration & Calibration

Configuration steps

  • Set ANGLE_THRESHOLD from 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_SIGMA from your own variance measurements while typing and while walking. It must sit clearly between the two.
  • Set REF_DRIFT_ALPHA conservatively. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

TestWhat you should see
Calibrate while sitting uprightThree short pulses, five seconds of stillness, one long confirmation pulse, and an angle reading near 0Β°.
Slouch forward deliberatelyAngle rises to 20–35Β°. No immediate response β€” the sustain timer must run first.
Hold the slouch for 50 secondsA single haptic pulse at about 45 seconds.
Keep slouchingTwo pulses at 105 s, three at 165 s, then ten minutes of silence.
Correct postureTimer resets immediately, no further nudges.
Reach forward for three seconds and returnNo response at all β€” this is the false positive the sustain timer exists to prevent.
Walk aroundSigma exceeds the threshold, monitoring is suspended, and no nudges occur regardless of angle.
Short press the buttonOne 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.

An ESP32 development board with the ESP-WROOM-32 module and USB connector
An ESP32 module. For week-long battery life, use the bare module and duty-cycle the sampling. Photograph sourced from Wikimedia Commons β€” ESP32 Espressif ESP-WROOM-32 Dev Board.jpg. Reused under the licence stated on that page; please check it before republishing.

Troubleshooting: Common Errors & Fixes

It buzzes constantly

Likely cause. Threshold too tight, sustain time too short, or the device has shifted since calibration.

Fix. Recalibrate first β€” a device that has rotated on its mount reports a large angle in perfectly good posture. Then raise the threshold to 22Β° and the sustain time to 60 s. A device set too aggressively is worse than none, because it gets removed.

It never buzzes even when slouching badly

Likely cause. Calibration was performed while already slouched, so the reference is the slouch.

Fix. Recalibrate in a deliberately good posture, ideally with someone else checking. Also verify the reference drift is only applied during good posture β€” if it runs unconditionally it will slowly follow you into a slouch.

The angle jumps around while typing

Likely cause. Mounting is not rigid, or dynamic acceleration is corrupting the gravity vector.

Fix. Mount to skin with an adhesive pad rather than clipping to a loose shirt. Enable the MPU-6050 digital low-pass filter at 10 Hz or below, and average several samples.

Battery lasts a day rather than a week

Likely cause. Continuous full-speed sampling on a development board.

Fix. Use a bare ESP32 module, drop to 4 Hz sampling, and use light sleep between samples. Deep sleep at night, woken by the MPU-6050 motion interrupt, roughly doubles the life again.

It nudges while walking

Likely cause. The variance gate threshold is too high, or the window is too short.

Fix. Log sigma while walking and set the threshold well below the minimum you observe. A two-second window at 10 Hz is a good compromise β€” shorter windows are noisy and longer ones respond too slowly when you stop.

The sketch will not upload β€” "Failed to connect" or "avrdude: stk500_recv()"

Likely cause. The bootloader is not being reached: wrong port, wrong board, a serial monitor holding the port open, or a USB cable that only carries power.

Fix. Close every serial monitor, confirm Tools β†’ Board and Port, and swap to a known data-capable USB cable. On an ESP32 hold BOOT while the IDE prints "Connecting…", then release. If a peripheral is wired to the UART pins (GPIO 1/3 on ESP32, D0/D1 on Uno) unplug it β€” it fights the programmer.

The board resets in a loop, or the serial monitor prints "Brownout detector was triggered"

Likely cause. The supply cannot deliver peak current. Wi-Fi transmit bursts, relay coils and servos all pull far more than their average draw.

Fix. Power peripherals from a separate regulated supply with a common ground rather than from the board 5 V pin. Add a 470–1000 Β΅F electrolytic capacitor across the supply near the load, and use a real power adapter rather than a laptop USB port.

Serial monitor shows garbage characters

Likely cause. Baud rate mismatch between Serial.begin() and the monitor, or a floating/shared UART line.

Fix. Set the monitor to 115200 to match the sketch. If it still garbles, the crystal or the USB bridge is being confused by noise β€” shorten the cable and keep motor wiring away from the USB lead.

An IΒ²C device is not detected

Likely cause. Wrong address, missing pull-ups, swapped SDA/SCL, or a bus too long for the pull-up value.

Fix. Run an IΒ²C scanner sketch first β€” it should print the device address. Most breakout boards include 4.7 kΞ© pull-ups, but if you have chained four of them the parallel resistance is too low; remove the pull-ups from all but one board. Keep the bus under 30 cm at 100 kHz.

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 a millis() 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_t where 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

Why not use the gyroscope for a better angle?

Because you do not need it and it costs battery. Integrating gyroscope rate to get angle drifts within seconds and requires fusion with the accelerometer to correct β€” which just brings you back to the gravity vector. For measuring orientation relative to gravity on a slow-moving body segment, the accelerometer alone is both simpler and more accurate.

Why 45 seconds and not immediate feedback?

Because immediate feedback makes the device unwearable. Reaching, bending and leaning all produce large angles for a few seconds and none of them are the problem. The posture that causes trouble is the sustained slump, and a delay filters everything else out. This single parameter is the difference between a device that gets worn and one that does not.

Where exactly should it go?

Upper thoracic, roughly T3–T5, between and slightly below the shoulder blades. Lower down measures lumbar flexion, which is a different and also useful measurement but needs a different reference and different thresholds. Wherever you choose, it must be the same place every session or the calibration is invalid.

Does the reference drift not just let me slouch more?

Only if you let it update during poor posture, which this firmware deliberately does not. Drift applies exclusively while the angle is within threshold, so it follows a genuinely changed neutral (new chair, new desk height) without following a bad habit. The time constant is also long β€” about half an hour of accumulated good posture.

Is there evidence these devices work?

Mixed, and worth being honest about. Studies generally find short-term improvement in posture while the device is worn, with less clear evidence of lasting change after removal, and inconsistent evidence on pain outcomes. What is better supported is the value of the data β€” knowing that your posture degrades after 40 minutes is actionable in a way that a buzz is not.

Can I use it during exercise?

Not usefully. The variance gate will suppress it constantly, and the gravity-vector measurement is invalid during dynamic movement. For lifting technique specifically, a version that triggers on the combination of high variance and large flexion is a genuinely useful and quite different project.

References & Learning Resources

These are the primary sources worth reading in full. Manufacturer datasheets always outrank forum posts when the two disagree.

  1. MPU-6000/6050 six-axis motion tracking device β€” datasheetTDK InvenSense
  2. O'Sullivan et al., "Neutral lumbar spine sitting posture in pain-free subjects"Manual Therapy, 2010
  3. Simpson et al., "The effect of wearable posture devices on posture and pain: a systematic review"Applied Ergonomics, 2019
  4. Accelerometer-based inclinometry β€” theory and limitationsAnalog Devices
  5. ISO 11226 β€” Ergonomics: evaluation of static working posturesISO