Siddhant Kumar
Project 025 · Health & Wearables

Stress & HRV Wearable.

A wrist band that reads heart-rate variability to estimate autonomic balance, guides paced breathing to shift it, and shows you — honestly — what HRV can and cannot tell you about stress.

Intermediate 12–18 hours 35 min read HRVWearableWellness
Jump to source Bill of materials
Stress & HRV Wearable — reference build illustration MCU VCC · GND · SIG · NC
Difficulty
Intermediate
Build time
12–18 hours
Indicative cost
₹2,600 – ₹3,300
Platform
ESP32 DevKit V1 (ESP-WROOM-32)
Category
Health & Wearables
Last updated
28 July 2026
Contents — 25 sections

Project Overview

A wrist band that reads heart-rate variability to estimate autonomic balance, guides paced breathing to shift it, and shows you — honestly — what HRV can and cannot tell you about stress.

Heart-rate variability is one of the more genuinely useful physiological signals a wearable can extract, and also one of the most over-claimed. The signal is real: the interval between heartbeats is not constant, and its variability reflects the balance between the two branches of the autonomic nervous system — sympathetic (fight-or-flight) and parasympathetic (rest-and-digest). When you are relaxed, the parasympathetic system modulates heart rate strongly with breathing, producing high variability. Under stress, sympathetic dominance suppresses this, and variability falls.

This device measures HRV properly from a photoplethysmogram, computes the standard time-domain and frequency-domain metrics, and — its most valuable feature — provides biofeedback. Slow, paced breathing at around six breaths per minute maximises the respiratory modulation of heart rate (respiratory sinus arrhythmia) and increases HRV in real time. The band guides this breathing and shows the HRV rising as you do it, which is both a genuine relaxation technique and a satisfying demonstration that the measurement is real.

The honesty is in the interpretation. HRV is highly individual — a "good" value for one person is a poor value for another, so only within-person comparison is meaningful. It is affected by age, fitness, position, time of day, hydration, and recent food and caffeine. A single reading tells you little; a consistent morning baseline over weeks tells you something. And "stress" as measured by HRV is autonomic arousal, which is not the same as psychological stress, though they correlate. The device presents HRV as a trend and a biofeedback target, not as a stress score to be taken literally.

Built on the same MAX30102 front end as the heart-rate band project, this build adds the frequency-domain analysis (LF/HF ratio) and the paced-breathing biofeedback loop that turns a passive measurement into an active tool.

A wrist-worn fitness tracker
A wrist wearable. HRV needs clean beat-to-beat timing, so good skin contact matters even more than for a bare heart-rate reading. 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 inter-beat intervals from a wrist PPG signal.
  • Computes time-domain HRV (RMSSD, SDNN, pNN50) and frequency-domain metrics (LF, HF, LF/HF).
  • Provides paced-breathing biofeedback at a configurable rate around six breaths per minute.
  • Shows HRV rising in real time during paced breathing.
  • Tracks a personal morning baseline for meaningful within-person comparison.
  • Rejects readings corrupted by motion, so it only reports on clean data.
  • Logs sessions and baseline trend over time.

Real-World Applications

SettingHow it is used
Stress-management biofeedbackPaced breathing with real-time HRV feedback is an evidence-supported relaxation technique.
Training recoveryMorning HRV baseline trends track autonomic recovery and readiness, widely used by athletes.
Mindfulness and breathing practiceObjective feedback that a breathing practice is actually shifting your physiology.
Autonomic function awarenessA window into the balance between the two branches of the nervous system.
Learning physiological signal analysisTime and frequency-domain HRV, including the subtle problem of spectral analysis on uneven samples.
Anxiety self-regulationA concrete, controllable target during moments of arousal.

Deployment contexts where a build of this kind earns its keep.

Features & Capabilities

  • Full HRV metric suite — time-domain and frequency-domain, computed correctly.
  • Paced-breathing biofeedback at the ~0.1 Hz resonance frequency that maximises HRV.
  • Real-time HRV display during breathing, closing the biofeedback loop.
  • Personal baseline tracking, because HRV is only meaningful within an individual.
  • Frequency analysis via Lomb-Scargle, the correct method for unevenly-sampled RR intervals.
  • Motion gating so metrics are computed only on clean, still data.
  • Coherence scoring — a measure of how rhythmic and resonant the HRV pattern is.
  • Honest framing — HRV as a personal trend and biofeedback target, not an absolute stress score.

Difficulty, Time & Required Skills

AttributeValue
Difficulty levelIntermediate
Estimated completion time12–18 hours
Indicative build cost₹2,600 – ₹3,300
Primary disciplineHealth & Wearables
Reference platformESP32 DevKit V1 (ESP-WROOM-32)

Skills you should have (or will pick up)

  • Arduino C++ with signal processing
  • PPG peak detection (from the heart-rate band project)
  • Time and frequency-domain HRV computation
  • The Lomb-Scargle periodogram for uneven sampling
  • BLE and simple UI/biofeedback design

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
MAX30102 pulse oximeter + heart-rate sensor
Contact pressure dominates signal quality — a loose sensor gives a beautiful-looking, meaningless waveform.
Red 660 nm + IR 880 nm LEDs, 18-bit ADC, 50–3200 SPS, on-chip temperature1₹380
ADXL345 3-axis accelerometer
Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz1₹220
0.96″ SSD1306 OLED display
Static images burn in — invert or scroll the screen periodically.
128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C1₹250
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
Vibration motor + driver
Haptic pacing lets you keep your eyes closed during a breathing session.
10 mm coin, for breathing pacing haptics1₹120
Wrist enclosure + strapTPU/PETG, sensor flush to skin, light seal1₹250
400 mAh LiPo cell3.7 V protected1₹220

Estimated total: ₹2,445, 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
MAX30102 pulse oximeter + heart-rate sensorRed 660 nm + IR 880 nm LEDs, 18-bit ADC, 50–3200 SPS, on-chip temperature1.8 V + 3.3 V (module regulates)I²C (0x57)Datasheet
ADXL345 3-axis accelerometer±2/4/8/16 g, 13-bit, 0.004 g/LSB, tap and free-fall interrupts, 3200 Hz2.0–3.6 VI²C / SPIDatasheet
0.96″ SSD1306 OLED display128 × 64 monochrome, 1.3–3.3 V logic, 100 kHz–400 kHz I²C3.3–5 VI²C (0x3C)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 gridDatasheet

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.
MAX30102 pulse oximeter + heart-rate sensor1.8 V + 3.3 V (module regulates)5.5Contact pressure dominates signal quality — a loose sensor gives a beautiful-looking, meaningless waveform.
ADXL345 3-axis accelerometer2.0–3.6 V0.14Built-in activity/free-fall interrupts let the MCU deep-sleep until something moves.
0.96″ SSD1306 OLED display3.3–5 V20Static images burn in — invert or scroll the screen periodically.
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 1185.6399999999999 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
ArduinoJson 7.xZero-allocation JSON serialisation and parsing.Library Manager → "ArduinoJson" by Benoit Blanchon
Adafruit SSD1306 + GFX 2.5.xFramebuffer and text/graphics primitives for the OLED.Library Manager → "Adafruit SSD1306"
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.

Stress & HRV Wearable — system block diagramFunctional block diagram of the Stress & HRV Wearable system. SenseMAX30102 PPGinter-beat intervalsADXL345motion gateExtractTime-domain HRVRMSSD, SDNNFrequency HRVLomb-Scargle LF/HFFeedbackPaced breathing~6 breaths/minCoherenceresonance scoreTrackPersonal baselinewithin-person onlyTrend logover weeksRR seriesmetricssession
Stress & HRV 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.

Stress & HRV 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 USBMAX30102 PPGGPIO 21 / 22 / 27I²C at 0x57ADXL345 motionGPIO 21 / 22Shared I²C, 0x53ButtonGPIO 33Start session / modeSSD1306 OLEDGPIO 21 / 22Shared I²C,breathing guideVibration motorGPIO 25Breathing pacinghapticStatus LEDGPIO 26Coherence indicator
Stress & HRV Wearable — wiring schematic
PeripheralPeripheral pinController pinSignal
MAX30102 PPGSDA / SCL / INTGPIO 21 / 22 / 27I²C at 0x57
ADXL345 motionSDA / SCLGPIO 21 / 22Shared I²C, 0x53
ButtonNOGPIO 33Start session / mode
SSD1306 OLEDSDA / SCLGPIO 21 / 22Shared I²C, breathing guide
Vibration motorTransistor baseGPIO 25Breathing pacing haptic
Status LEDAnodeGPIO 26Coherence indicator

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 MAX30102 must contact skin directly with no air gap and a light seal around it — exactly as in the heart-rate band project. HRV needs clean, precise R-peak timing, so signal quality matters even more here than for a bare heart rate.
  • The accelerometer must be rigidly attached to the same body as the PPG sensor so its motion signal corresponds to the sensor's motion for gating.
  • A green LED PPG channel (if your module has one) is more motion-robust for beat detection; red/IR is fine for still HRV sessions, which is the intended use.
  • Keep the I²C at 400 kHz for reliable high-rate PPG sampling.
  • The vibration motor for breathing pacing needs a transistor and flyback diode; a GPIO cannot drive it.
  • For a wrist device intended for still seated sessions, motion is less of a problem than for continuous wear — but the gating still matters because even small wrist movements corrupt the fine RR timing HRV depends on.
An electrocardiogram waveform trace
A physiological trace. HRV analyses the variation in the interval between successive beats, not the beat rate itself. Photograph sourced from Wikimedia Commons — ECG trace.png. Reused under the licence stated on that page; please check it before republishing.

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.

Stress & HRV Wearable — architecture stackLayered architecture from hardware to user interface. Hardware layerESP32 DevKit V1 (ESP-WROOM-32) · MAX30102 pulse oximeter + heart-ratesensorDriver layerarduinojson · ssd1306 · preferencesApplication logicsampling loop · filtering · thresholds · state machinePresentation layerlocal display · serial console · logged output
Stress & HRV Wearable — architecture stack

Working Principle

Heart rate is not a metronome. Even at rest, the interval between successive beats varies from one beat to the next, and that variation carries information about the autonomic nervous system. The parasympathetic branch (via the vagus nerve) can change heart rate quickly, beat to beat, and is strongly influenced by breathing — heart rate rises on inhalation and falls on exhalation, a phenomenon called respiratory sinus arrhythmia. The sympathetic branch acts more slowly. When you are relaxed, vagal tone is high and beat-to-beat variability is large; under stress or exertion, sympathetic dominance suppresses the fast variability.

The time-domain metrics quantify this directly from the RR intervals. RMSSD (root mean square of successive differences) captures the fast beat-to-beat changes and so primarily reflects parasympathetic activity — it is the metric most used for short recordings and biofeedback. SDNN (standard deviation of all intervals) captures overall variability across all timescales. pNN50 (the proportion of successive intervals differing by more than 50 ms) is another parasympathetic index.

The frequency-domain metrics decompose the variability by rhythm. High-frequency (HF, 0.15–0.4 Hz) power corresponds to the respiratory rhythm and reflects parasympathetic activity. Low-frequency (LF, 0.04–0.15 Hz) power reflects a mix of sympathetic and parasympathetic influence and a baroreflex rhythm around 0.1 Hz. The LF/HF ratio is sometimes taken as a "sympathovagal balance", though this interpretation is contested and should be treated cautiously.

Computing the frequency spectrum correctly requires care, because RR intervals are unevenly sampled — they occur at the (irregular) beat times, not at a fixed rate. Applying an ordinary FFT requires first interpolating onto an even grid, which distorts the spectrum. The Lomb-Scargle periodogram computes the spectrum directly from unevenly-sampled data without interpolation, and is the statistically correct method for HRV frequency analysis.

The biofeedback is where this becomes an active tool. There is a resonance frequency of the cardiovascular system, typically around 0.1 Hz (six breaths per minute), at which breathing produces the largest heart-rate oscillations. Breathing at this rate maximises HRV and drives the system into a state of high "coherence" — a smooth, sinusoidal heart-rate pattern. This is both a measurable phenomenon and an effective relaxation technique: the device paces your breathing to this rate and shows the HRV rising as you do it, closing a genuine biofeedback loop.

The essential caveat is individuality. HRV varies enormously between people — a healthy 25-year-old athlete and a healthy 60-year-old can differ by a factor of five — so absolute values are almost meaningless across individuals. It also varies within a person by time of day, position, breathing, hydration, recent food and caffeine, and recent exercise. This is why the device tracks a personal baseline (ideally measured each morning in a consistent position) and reports change relative to that, never an absolute "stress score" compared to a population.

The maths behind it

Time-domain HRV

plainTime-domain HRV
Given RR intervals (ms): RR[1..N]

  meanRR = (1/N) Σ RR
  SDNN   = sqrt( (1/(N-1)) Σ (RR - meanRR)² )
  RMSSD  = sqrt( (1/(N-1)) Σ (RR[i+1] - RR[i])² )
  pNN50  = 100 · count(|RR[i+1]-RR[i]| > 50 ms) / (N-1)

RMSSD is the primary short-term / biofeedback metric
(parasympathetic). Typical resting adult RMSSD: 20–90 ms,
but only compare a person against themselves.

Lomb-Scargle periodogram (uneven sampling)

plainLomb-Scargle periodogram (uneven sampling)
RR intervals occur at beat times t_k, not on an even grid,
so an FFT would require interpolation and distort the spectrum.

For frequency ω, the Lomb-Scargle power:
  τ chosen so that Σ sin(2ω(t_k − τ)) = 0
  P(ω) = (1/2)[ (Σ x_k cos ω(t_k−τ))² / Σ cos²ω(t_k−τ)
             + (Σ x_k sin ω(t_k−τ))² / Σ sin²ω(t_k−τ) ]

Integrate P(ω) over:
  LF band: 0.04–0.15 Hz     HF band: 0.15–0.40 Hz
  LF/HF = LF power / HF power

Coherence and resonance

plainCoherence and resonance
Resonance frequency ≈ 0.1 Hz → 6 breaths/min
(period 10 s: 4–5 s inhale, 5–6 s exhale is comfortable)

Coherence: how concentrated the HRV spectrum is around
the breathing frequency.
  coherence = peak_power_in_0.04-0.26Hz
            / total_power_in_0.04-0.26Hz

High coherence (smooth, sinusoidal heart-rate pattern)
is the biofeedback target during paced breathing.

Program Flowchart

The firmware is a single cooperative loop. Nothing blocks for long, so networking, sensing and the user interface all stay responsive.

Stress & HRV Wearable — firmware flowchartControl flow through the main program loop. Boot: load personal baselineRead PPG, detect beats, buildRR seriesWrist still and signalclean?yespause, prompt stillCompute time and frequency HRVBreathing session active?guide paced breathingshow metricsPace at ~6 breaths/min, showlive HRVScore coherence, updatebaselineLog session, sleep
Stress & HRV 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.

cppstress-hrv-band.ino
/* ═══════════════════════════════════════════════════════════════
   Stress & HRV Wearable — ESP32 + MAX30102 + ADXL345

   Computes time-domain and frequency-domain HRV, and provides paced-
   breathing biofeedback at the ~0.1 Hz resonance frequency.

   HRV is highly individual. Compare a person only against their own
   baseline. This is a wellness and biofeedback tool, not a diagnosis.
   ══════════════════════════════════════════════════════════════════ */

#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_ADXL345_U.h>
#include <Preferences.h>
#include <math.h>

#define PIN_VIBE 25
#define PIN_BTN  33
#define PIN_LED  26

#define RR_MAX      128
#define BREATH_S    10.0f          // 6 breaths/min resonance

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Adafruit_ADXL345_Unified accel(4);
Preferences prefs;

// (PPG beat detection from the heart-rate band project supplies this.)
extern bool ppgBeat(uint32_t &rrMs);      // returns true on a new beat, gives RR
extern bool ppgClean();                   // signal quality gate
extern float accelVariance();

float    rr[RR_MAX];
uint32_t rrT[RR_MAX];                       // beat timestamps for Lomb-Scargle
int      rrCount = 0;
float    rmssd = 0, sdnn = 0, lf = 0, hf = 0, lfhf = 0, coherence = 0;
float    baselineRmssd = 0;
bool     breathing = false;

/* ── time-domain ────────────────────────────────────────────── */
void timeDomain() {
  if (rrCount < 8) return;
  float mean = 0; for (int i = 0; i < rrCount; i++) mean += rr[i]; mean /= rrCount;
  double var = 0, succ = 0;
  for (int i = 0; i < rrCount; i++) { float d = rr[i] - mean; var += d * d; }
  for (int i = 1; i < rrCount; i++) { float d = rr[i] - rr[i - 1]; succ += (double)d * d; }
  sdnn  = sqrt(var / (rrCount - 1));
  rmssd = sqrt(succ / (rrCount - 1));
}

/* ── frequency-domain via Lomb-Scargle ──────────────────────── */
float lombPower(float freq) {
  float w = 2.0f * (float)M_PI * freq;
  float mean = 0; for (int i = 0; i < rrCount; i++) mean += rr[i]; mean /= rrCount;

  // tau such that the sine sum vanishes.
  float s2 = 0, c2 = 0;
  for (int i = 0; i < rrCount; i++) {
    float t = rrT[i] / 1000.0f;
    s2 += sinf(2 * w * t); c2 += cosf(2 * w * t);
  }
  float tau = atan2f(s2, c2) / (2 * w);

  float xc = 0, xs = 0, cc = 0, ss = 0;
  for (int i = 0; i < rrCount; i++) {
    float t = rrT[i] / 1000.0f;
    float x = rr[i] - mean;
    float co = cosf(w * (t - tau)), si = sinf(w * (t - tau));
    xc += x * co; xs += x * si; cc += co * co; ss += si * si;
  }
  return 0.5f * ((cc > 0 ? xc * xc / cc : 0) + (ss > 0 ? xs * xs / ss : 0));
}

void frequencyDomain() {
  if (rrCount < 20) return;                 // need enough beats for the spectrum
  lf = hf = 0;
  float peak = 0, total = 0;
  for (float f = 0.04f; f <= 0.40f; f += 0.005f) {
    float p = lombPower(f);
    if (f < 0.15f) lf += p; else hf += p;
    if (f <= 0.26f) { total += p; if (p > peak) peak = p; }
  }
  lfhf = hf > 0 ? lf / hf : 0;
  coherence = total > 0 ? peak / total : 0;
}

/* ── paced-breathing biofeedback ────────────────────────────── */
void breathingGuide(uint32_t now) {
  // A rising/falling bar paced at the resonance frequency, plus haptics
  // at the inhale/exhale transitions so you can keep your eyes closed.
  float phase = fmodf(now / 1000.0f, BREATH_S) / BREATH_S;   // 0..1
  bool inhale = phase < 0.45f;
  static bool wasInhale = false;
  if (inhale != wasInhale) { digitalWrite(PIN_VIBE, HIGH); delay(60); digitalWrite(PIN_VIBE, LOW); wasInhale = inhale; }

  oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE);
  oled.setTextSize(2); oled.setCursor(0, 0);
  oled.print(inhale ? "Breathe IN" : "Breathe OUT");

  int barH = inhale ? (int)(phase / 0.45f * 30) : (int)((1 - (phase - 0.45f) / 0.55f) * 30);
  oled.fillRect(0, 30, barH * 4, 8, SSD1306_WHITE);

  oled.setTextSize(1);
  oled.setCursor(0, 44); oled.printf("RMSSD %.0f ms", rmssd);
  oled.setCursor(0, 54); oled.printf("coherence %.2f", coherence);
  oled.display();

  // LED brightens with coherence — a glanceable biofeedback signal.
  analogWrite(PIN_LED, (int)(coherence * 255));
}

/* ── setup / loop ───────────────────────────────────────────── */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_VIBE, OUTPUT);
  pinMode(PIN_BTN, INPUT_PULLUP);
  pinMode(PIN_LED, OUTPUT);
  Wire.begin(21, 22); Wire.setClock(400000);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  accel.begin(0x53);
  prefs.begin("hrv", false);
  baselineRmssd = prefs.getFloat("base", 0);
  Serial.println("HRV band — compare to YOUR baseline only");
}

void loop() {
  if (digitalRead(PIN_BTN) == LOW) { delay(50); if (digitalRead(PIN_BTN) == LOW) {
    breathing = !breathing; while (digitalRead(PIN_BTN) == LOW) delay(20); } }

  uint32_t now = millis();
  uint32_t rrMs;
  if (ppgClean() && accelVariance() < 0.15f && ppgBeat(rrMs)) {
    if (rrMs > 300 && rrMs < 2000) {
      // Slide the RR window.
      if (rrCount >= RR_MAX) {
        memmove(rr, rr + 1, sizeof(float) * (RR_MAX - 1));
        memmove(rrT, rrT + 1, sizeof(uint32_t) * (RR_MAX - 1));
        rrCount--;
      }
      rr[rrCount] = rrMs;
      rrT[rrCount] = now;
      rrCount++;
      timeDomain();
    }
  }

  static uint32_t lastFreq = 0;
  if (now - lastFreq > 5000) { lastFreq = now; frequencyDomain(); }

  if (breathing) {
    breathingGuide(now);
  } else {
    oled.clearDisplay(); oled.setTextColor(SSD1306_WHITE);
    oled.setTextSize(1); oled.setCursor(0, 0); oled.print("HRV");
    oled.setTextSize(2); oled.setCursor(0, 12); oled.printf("%.0f ms", rmssd);
    oled.setTextSize(1);
    oled.setCursor(0, 34);
    if (baselineRmssd > 0)
      oled.printf("baseline %.0f (%+.0f%%)", baselineRmssd,
                  (rmssd - baselineRmssd) / baselineRmssd * 100);
    else oled.print("no baseline yet");
    oled.setCursor(0, 46); oled.printf("LF/HF %.2f  SDNN %.0f", lfhf, sdnn);
    oled.setCursor(0, 56); oled.print("Personal trend only");
    oled.display();
  }

  static uint32_t lastLog = 0;
  if (now - lastLog > 3000) {
    lastLog = now;
    Serial.printf("RMSSD %.0f  SDNN %.0f  LF/HF %.2f  coh %.2f  n=%d\n",
                  rmssd, sdnn, lfhf, coherence, rrCount);
  }
}
lombPower() — Lomb-Scargle, not FFTRR intervals occur at irregular beat times, so an FFT would require interpolating onto an even grid, distorting the spectrum. The Lomb-Scargle periodogram computes the spectral power directly from unevenly-sampled data, which is the statistically correct method for HRV frequency analysis.
RMSSD as the primary metricRMSSD captures beat-to-beat changes and so tracks parasympathetic (vagal) activity, which responds fast and is what paced breathing modulates. It is the most appropriate metric for short recordings and biofeedback, which is why it drives the live display.
accelVariance() < 0.15 gateHRV depends on precise R-peak (here, PPG pulse) timing, and even small wrist movements corrupt that timing. Gating on low motion ensures the metrics reflect physiology, not artefact — a moving wrist produces a fake "low HRV / high stress" reading.
BREATH_S = 10 s (6 breaths/min)This is the cardiovascular resonance frequency, around 0.1 Hz, at which breathing produces the largest heart-rate oscillations and maximises HRV. Pacing to it is both a demonstration that the measurement is real and an effective relaxation technique.
coherence drives the LED brightnessCoherence measures how concentrated the HRV spectrum is around the breathing frequency — a smooth, resonant heart-rate pattern. Mapping it to LED brightness gives a glanceable biofeedback signal you can watch with your eyes half-closed during a breathing session.
baseline comparison as percent changeThe display shows RMSSD relative to the personal baseline, not as an absolute number, because HRV is only meaningful within an individual. "Personal trend only" is on screen to reinforce that a value that is good for one person is poor for another.
Haptic pulse at inhale/exhale transitionsThe vibration at each breathing transition lets the user pace their breathing with eyes closed, which is how relaxation biofeedback is actually practised — staring at a screen defeats the purpose.

Configuration & Calibration

Configuration steps

  • Set the PPG front end up as in the heart-rate band project — good skin contact and a light seal are prerequisites, because HRV needs clean beat timing.
  • Set BREATH_S to the user's resonance frequency. Six breaths per minute (10 s) is the population average, but individual resonance ranges from about 4.5 to 7 breaths per minute; a slower sweep to find each person's resonance is worthwhile.
  • Establish a personal baseline by taking several morning readings in a consistent position (seated or lying) and averaging them. Only compare against this.
  • Set the motion gate from your own accelerometer variance while sitting still — HRV sessions should be still, so the gate can be fairly tight.
  • Collect at least 20 beats (about 20 seconds) before trusting the frequency-domain metrics; more is better for spectral resolution.

Calibration procedure

An uncalibrated sensor produces confident, precise, wrong numbers. Do this once per physical unit and record the constants.

  1. Find your resonance frequency

    Breathe at 4.5, 5, 5.5, 6, 6.5 and 7 breaths per minute for two minutes each and note which gives the highest RMSSD and coherence. That is your personal resonance frequency; set the pacing to it. It is usually between 5 and 6.5 breaths per minute.

  2. Establish a morning baseline

    For a week, take a five-minute reading each morning immediately after waking, in the same position, before caffeine or exercise. Average the RMSSD values. This baseline is the only meaningful reference for your subsequent readings.

  3. Validate the metrics against an ECG if possible

    If you built the ECG project, compare HRV metrics from both on the same session. PPG-derived HRV is slightly noisier than ECG-derived, but for still biofeedback sessions they should agree closely. Large differences point to PPG signal-quality problems.

  4. Confirm the biofeedback loop works

    Do a paced-breathing session and watch RMSSD and coherence rise as you settle into the rhythm. If they do not respond, the beat timing is too noisy — improve the PPG contact and light seal.

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
Sit still and read HRV at restA stable RMSSD in a physiological range, repeatable across a session.
Do a paced-breathing session at ~6 breaths/minRMSSD and coherence rise noticeably as you settle into the rhythm — the biofeedback loop closing.
Compare metrics to a still baselineThe display shows change relative to the personal baseline, not an absolute score.
Move your wrist during a readingThe motion gate pauses metric computation rather than reporting a corrupted (falsely low) HRV.
Breathe at a fast, shallow rateHF power and RMSSD fall; the metrics respond to the change in breathing, demonstrating they are real.
Check the frequency-domain metrics after 30 sPlausible LF and HF power with LF/HF around 1–2 at rest; during resonant breathing, HF and coherence dominate.
Take morning readings over a weekA baseline emerges; day-to-day variation is normal, and a sustained drop may reflect fatigue or illness.
Verify against the ECG project if builtClose agreement in RMSSD for still sessions, confirming the PPG HRV is trustworthy.

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

HRV values swing wildly

Likely cause. Noisy PPG beat detection or motion.

Fix. HRV is exquisitely sensitive to beat-timing noise — a single misdetected or extra beat produces a huge spurious RMSSD. Improve the PPG signal (contact, light seal, LED current), gate hard on motion, and reject non-physiological RR intervals (outside 300–2000 ms).

The frequency-domain metrics look wrong

Likely cause. Too few beats, or an FFT applied to interpolated data.

Fix. Use the Lomb-Scargle periodogram directly on the RR intervals, not an FFT on interpolated data. Collect at least 20–30 beats for adequate spectral resolution. For proper LF resolution you actually want several minutes of data — short-window LF is inherently uncertain.

Coherence does not rise during paced breathing

Likely cause. Wrong breathing rate, or noisy beat timing masking the effect.

Fix. Find the individual's resonance frequency — it may not be exactly six breaths per minute. Ensure the PPG timing is clean enough to resolve the respiratory modulation. Some people take several sessions to learn to breathe smoothly enough to raise coherence.

My HRV is much lower than my friend's — am I unhealthy?

Likely cause. Comparing HRV between people, which is not meaningful.

Fix. This is not a bug, it is a misunderstanding of HRV. Absolute HRV varies enormously between individuals with age, fitness and genetics. A value that is low for one person is high for another. Only compare against your own baseline over time.

Morning readings vary a lot day to day

Likely cause. Normal — HRV is genuinely variable.

Fix. Day-to-day variation is expected and normal. Look at the trend over a week or more, not single readings. A sustained drop below your baseline over several days may reflect fatigue, illness or poor sleep; a single low reading means little.

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

  • Compute time-domain metrics on every new beat (cheap) but frequency-domain metrics only every few seconds (the Lomb-Scargle is more expensive).
  • Keep a rolling RR window rather than recomputing from scratch, and cap it at a couple of minutes — longer windows blur genuine changes.
  • Gate hard on motion. A clean 30-second still reading gives better HRV than a noisy five-minute one.
  • 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 wellness and biofeedback tool, not a medical device. HRV is not a diagnosis of anything, and a low reading does not mean you are ill.
  • Do not use HRV readings to make medical decisions or to override how you actually feel. If you are unwell, seek care regardless of what your HRV shows.
  • Paced slow breathing is generally safe and relaxing, but if you feel dizzy or lightheaded, stop and breathe normally — do not force a rate that is uncomfortable.
  • HRV can be a useful signal for training recovery, but it is one input among many; do not let a number override good judgement about rest and load.
  • 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 resonance-frequency finder that automatically sweeps breathing rates and identifies each user's personal resonance.
  • Add guided sessions of varying length with progress tracking, turning it into a proper biofeedback trainer.
  • Add sleep HRV measurement — overnight HRV is the most standardised and informative, free of the confounds of daytime activity.
  • Add ECG input (from the ECG project) as an option, giving gold-standard beat timing for the most accurate HRV.
  • Add a readiness score combining morning HRV, resting heart rate and their trends — but present it as guidance, never as a command.
  • 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

What is a good HRV number?

There isn't a universal one, and this is the most important thing to understand about HRV. Absolute values vary by a factor of several between healthy people depending on age, fitness and genetics — a fit 25-year-old might have an RMSSD of 80 ms and a healthy 60-year-old 25 ms, both perfectly normal. HRV is only meaningful compared against your own baseline over time. Chasing someone else's number is meaningless.

Does HRV really measure stress?

It measures autonomic arousal — the balance between the sympathetic and parasympathetic nervous systems — which correlates with stress but is not identical to it. Physical exertion, illness, poor sleep, caffeine and even standing up all lower HRV without any psychological stress. So HRV is a useful physiological signal that relates to stress, but "your stress is 73" is an over-simplification the device deliberately avoids.

Why breathe at six breaths per minute?

Because that is roughly the resonance frequency of the cardiovascular system, where breathing produces the largest heart-rate oscillations and maximises HRV. Breathing at this rate drives the heart-rate pattern into a smooth, high-amplitude sinusoid — high "coherence" — which is both a relaxation technique and a satisfying, visible demonstration that your breathing is directly controlling your physiology. Your personal resonance may be slightly different, between about 4.5 and 7 breaths per minute.

Why not just use an FFT for the frequency analysis?

Because RR intervals are unevenly sampled — they occur at the irregular beat times, not on a fixed clock. An FFT needs evenly-spaced samples, so you'd have to interpolate first, which distorts the spectrum. The Lomb-Scargle periodogram computes the spectrum directly from unevenly-sampled data and is the statistically correct method. It is a real, if subtle, point that a lot of HRV code gets wrong.

Is PPG-based HRV as good as ECG?

For still, seated biofeedback sessions, close. PPG measures the pulse arriving at the wrist, which is a slightly noisier proxy for the heartbeat than the ECG's direct electrical R peak, so PPG HRV has a bit more timing jitter. During movement the difference is large — PPG is much more motion-sensitive. For the intended use here (still sessions) PPG is fine; for the most accurate HRV, use the ECG project's input.

My HRV dropped this morning — should I worry?

Not from a single reading. Day-to-day HRV variation is large and normal. A single low morning reading could be poor sleep, a late meal, alcohol, or nothing at all. What is informative is a sustained trend — a drop maintained over several days can reflect accumulated fatigue, overtraining or an oncoming illness. Look at the trend, not the point, and never let a number override how you actually feel.

References & Learning Resources

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

  1. Shaffer & Ginsberg, "An Overview of Heart Rate Variability Metrics and Norms"Frontiers in Public Health, 2017
  2. Task Force of ESC/NASPE, "Heart rate variability: standards of measurement"Circulation, 1996
  3. Lehrer & Gevirtz, "Heart rate variability biofeedback: how and why does it work?"Frontiers in Psychology, 2014
  4. Lomb, "Least-squares frequency analysis of unequally spaced data"Astrophysics and Space Science, 1976
  5. MAX30102 pulse oximeter and heart-rate sensor — datasheetAnalog Devices
  6. Laborde et al., "Heart Rate Variability and Cardiac Vagal Tone in Psychophysiological Research"Frontiers in Psychology, 2017