A self-contained overnight heart rate, SpO2, and movement tracker built on an ESP32. The device records biometric data while you sleep and serves a polished single-page dashboard directly from itself over your local WiFi — no cloud, no account, no app. Open http://sleep-tracker.local from any phone or laptop on the same network and you get a modern responsive SPA with live waveforms, multi-night history, sleep-stage timeline, smart-alarm scheduling, and CSV export.
Not a medical device. Heuristic sleep staging is for personal exploration only. Do not use this to diagnose or treat any condition.
https://gagndeep.github.io/esp32-sleep-tracker/installer/
Plug your ESP32-C3 into a desktop running Chrome / Edge / Opera, click the button, and you're flashed and on WiFi in under a minute. Uses ESP Web Tools + Improv-Serial — no IDE, no terminal.
| Component | Role |
|---|---|
| ESP32-WROOM-32 dev board | MCU + WiFi |
| MAX30102 breakout | Heart rate + SpO2 (I2C) |
| MPU6050 breakout | Movement / accelerometer (I2C) |
| MicroSD SPI module | Long-term archive |
| Active piezo buzzer | Smart alarm + low-SpO2 alert |
| Status LED + optional button | UX |
Wiring details and pin assignments live in docs/wiring.md and firmware/include/pins.h.
# 1. Build the web bundle (Node 20+)
cd web && npm install && npm run build
# 2. Build & flash firmware (PlatformIO)
cd ..
pio run -t upload # firmware
pio run -t uploadfs # SPA assets to LittleFS
# 3. First boot: phone connects to AP "SleepTracker-XXXX",
# captive portal opens the Setup wizard, pick your home WiFi.
# 4. Open http://sleep-tracker.local on any device on the LAN.Full flashing & OTA upgrade steps: docs/flashing.md.
Data format for DIY analysis: docs/data-format.md.
firmware/— PlatformIO project (Arduino-ESP32 framework).web/— Preact + Tailwind + uPlot SPA. Build emits gzipped assets intofirmware/data/.docs/— wiring diagrams, flashing notes, data format reference.tools/analyze.py— optional offline analysis of CSV exports.
- Live HR / SpO₂ / activity stream over WebSockets (1 Hz)
- Heuristic sleep staging (awake / light / deep)
- Smart-alarm window with day-mask + volume ramp + breathing exercise (4-7-8)
- Multi-night history with sortable / searchable list
- 28-day Trends view with 7-day rolling average
- Compare two sessions side-by-side
- Session tags (sick / workout / alcohol / travel / caffeine / medication) + free-form notes
- CSV + raw-binary export per session
- Dark / light theme switcher (persists in
localStorage) - Timezone-aware rendering (preset list + custom IANA id)
- Optional 4-digit PIN auth for mutating endpoints
- OTA firmware update with MD5 verification + 30 s pending-verify rollback
- Manual
POST /api/ota/rollbackto revert to the previous partition - Crash-recovery via
<id>.startanchor file (correct UTC start after power loss) - Sidecar schema v2 (
tags,notes,started_at_unix,tz_offset_min,hrv_rmssd, ...) - WiFi scan with signal bars during setup
- Task watchdog (10 s) on sensor + pipeline tasks
- WebSocket back-pressure with per-client drop counter (
ws_dropsin/api/status) - Atomic
settings.jsonsave with CRC32 +.bakfallback - HRV coherence (HeartMath-style) with live score / level / breathing rate
The firmware ships a HeartMath-style HRV coherence pipeline that runs alongside the existing HR / SpO₂ / staging path. It measures how sinusoidal the IBI (inter-beat-interval) series is in the 0.04–0.26 Hz band — the band where slow-paced breathing entrains heart rate — and exposes:
- a coherence ratio = peak-band-power / (broadband-power − peak-band-power)
- a 0–16 score (saturating at ratio = 4)
- a Low / Med / High classification (HeartMath thresholds: Low <0.5, Med 0.5–2.5, High >2.5)
- a cumulative achievement counter that advances only while in Med or High
- the dominant frequency (Hz) inside the band — multiply by 60 for breaths/min
The Live tab renders a card with the score, level, breathing rate, and a 60 s ratio sparkline. Frames are pushed every 5 s.
WebSocket frame, 192-byte buffer:
{"type":"coherence","ratio":1.42,"score":11,"level":2,"ach":47,"f0":0.10,"sec":1834}/api/status includes a matching coherence block (plus an enabled
flag); /api/debug/coherence mirrors the shape of /api/debug/hr and
adds peak_power, total_power, IBI filter counters, and the most
recent 8 IBIs feeding the spline.
All knobs live in firmware/include/config.h under the
Coherence DSP block — window size, FFT size, peak / broadband
bands, classification thresholds, IBI rejection percentage. The
COHERENCE_ENABLED constant gates the FreeRTOS task.
Build with -DCOHERENCE_TEST_MODE=1 to compile in three synthetic IBI
generators. Select via settings.coherence_test_signal (persisted
to NVS, applied at boot):
| Setting | Signal | Expected outcome |
|---|---|---|
0 |
Real sensor input | normal |
1 |
0.10 Hz sinusoid (±100 ms) | ratio > 5, score = 16, level = High |
2 |
White noise (±50 ms) | ratio < 0.3, score = 0, level = Low |
3 |
0.25 Hz sinusoid (±100 ms) | clean peak at 0.25 Hz, dominantHz is the diagnostic |
Synthetic mode bypasses IbiQualityFilter so test B's white noise
isn't half-rejected, and short-circuits real beats from the sensor so
the test signal owns the pipeline.
- IBI source: the spec describes a fresh PPG → 0.5–8 Hz BPF →
slope-sum peak detector. We reuse the existing
HeartRateEstimator::popBeatIntervalMsIBI stream because it's already millisecond-precise and well tuned. The 400 Hz raw FIFO drain (stage 9) is wired but inert — foundation for a future slope-sum detector if HR detection is ever swapped. - Transport: the published spec prefers BLE GATT / MQTT / Serial-JSON. We reuse the existing WebSocket + REST stack. No new BLE service.
- Sensor rate:
MAX30102_SAMPLE_HZis 400 Hz withSENSOR_DECIMATION=4, so HR/SpO₂ still see an effective 100 Hz Reading stream.
Dark / light mode screenshots:
![]()
(Drop the renders into
docs/screenshots/once you have a built unit; the placeholders are intentionally not committed.)
docs/wiring.md— pin map, ASCII diagram, watchdog + PIN notes.docs/flashing.md— first flash, OTA + rollback, PIN auth.docs/data-format.md—Samplelayout, schema v2 sidecar JSON, anchor file, settings CRC.docs/troubleshooting.md—ws_drops, watchdog, anchor recovery, SD eject behaviour, factory reset.tools/analyze.py— plot a CSV export with matplotlib (renders tags in the title).
Personal-use project. No warranty, no medical claims. Add a LICENSE
of your choice (MIT is a good default for hobbyist hardware projects).