Data tiers
Not all Oura features can be replicated from raw BLE data. We sort them into three tiers.
Ring-computed metrics
The ring's firmware computes these; we decode the BLE packets.
- Sleep hypnogram (5-min bins)
- HRV (nightly avg + intervals)
- Resting heart rate
- SpO2, respiratory rate
- Body temperature deviation
- Steps, active calories
Locally computed 0–100 scores
Formula-based. ~±3–8 pts vs official after 14-day baseline. Labeled ~78 to distinguish from cloud scores.
- Sleep Score
- Readiness Score
- Activity Score
- Sleep debt
- VO₂ max estimate
Cloud ML models
These require Oura's proprietary PyTorch models or clinical data not in the BLE stream.
- Resilience Score
- Cardiovascular Age
- Workout auto-detection
- Cycle prediction
Data pipeline
BLE data flows through a typed pipeline before reaching the UI. All scoring happens in TypeScript and Rust on-device — nothing is sent to a server.
Oura Ring (Bluetooth) ↓ AES-128-ECB auth · TLV packet framing (open_oura, MIT) OuraBlePlugin.java ← Android GATT transport ↓ JNI bridge (Rust) native/oura-jni ← oura-protocol · oura-analysis ↓ bleEventMapper.ts ← RingEventEntry[] → typed records bleBaseline.ts ← 30-day rolling HRV / RHR / sleep baseline bleScoring.ts ← formula-based 0–100 scores ↓ ouraDataStore.ts ← same schema as cloud API path ↓ UI hooks (useReadiness / useSleepSessions …)
Sleep Score
Seven contributors, each mapped to 0–100, combined as a weighted average. Calibration on 20 days of real Oura data: ±2.7 pts MAE vs official.
Oura's official Sleep Score is produced by a cloud ML model trained on population data. Our formula uses the same inputs and documented contributor weights but will not be identical to the official number.
| Contributor | Weight | Formula |
|---|---|---|
| Total sleep | 7 h = 100, linear from 5 min = 0 | |
| Efficiency | % of time in bed spent asleep (ring-computed) | |
| Restfulness | 0 disruptions = 100; −10 per restless period | |
| REM sleep | 90 min = 100, linear from 0 | |
| Deep sleep | 60 min = 100, linear from 0 | |
| Latency | ≤5 min = 100; ≥60 min = 0; inverted linear | |
| Timing | Sleep midpoint within 1 h of 2 AM = 100; ≥3 h away = 0 |
Readiness Score
Seven contributors. HRV and RHR sub-scores require a 30-day personal baseline — the score confidence increases over the first 14 days. Calibration: ±5.3 pts MAE vs official.
| Contributor | Weight | Formula |
|---|---|---|
| Previous night | Last night's sleep score (above) | |
| HRV balance | recent_HRV ÷ 30-day baseline × 100 | |
| Body temperature | Nightly deviation from ring (exact, Tier 1) | |
| Sleep balance | last night ÷ 14-day sleep avg × 100 | |
| Resting HR | (baseline_RHR − recent + 10) ÷ 20 × 100 | |
| Recovery index | From sleep stage transition quality (ring delta) | |
| Activity balance | vs 7-day activity average |
Activity Score
Five contributors. Note: Oura's move every hour contributor tracks per-hour inactivity alerts — this data is not in the BLE stream, so that sub-score defaults to a neutral 70. This is the largest source of activity score error.
| Contributor | Weight | Formula |
|---|---|---|
| Meet daily targets | meters completed ÷ meters target; calorie fallback | |
| Stay active | non-sedentary time ÷ waking time × 143 | |
| Move every hour | Fixed at 70 — per-hour data not in BLE stream | |
| Training frequency | high-intensity time ÷ 2700 s (45 min target) | |
| Training volume | distance ÷ 8 km, or steps ÷ 10 000 |
Accuracy
Calibrated against 20 days of real Oura cloud data from a Gen3 ring. MAE = mean absolute error in score points (0–100 scale).
Weight optimization requires ≥70 data points per dimension (10× the number of free parameters). The calibration script re-runs automatically when this threshold is met.
| Dimension | MAE | Notes |
|---|---|---|
| Sleep Score | ±2.7 pts | Within Oura's own test-retest variance (~3 pts) |
| Readiness Score | ±5.3 pts | Higher variance due to body_temperature approximation in first 14 days |
| Activity Score | ±32 pts | move_every_hour sub-score fixed at 70 (no per-hour BLE data) |
| HRV (nightly avg) | Exact | Decoded directly from ring firmware (Tier 1) |
| SpO2 | ±2% | Ring-computed but calibration coefficients are ring-specific and not in open_oura |
Known gaps vs Oura cloud
Scores labeled ~ are approximations. They are never presented as Oura's official numbers. Oura's 0–100 scores are produced by proprietary PyTorch models trained on population data — we cannot replicate these without Oura's model weights.
Features unavailable without cloud
| Feature | Why unavailable |
|---|---|
| Resilience Score | Requires daytime HRV context + cloud ecore + 14-day PyTorch accumulation |
| Cardiovascular / Vascular Age | PPG pulse wave velocity via deep learning (NUS-validated model); ring-specific PPG calibration also needed |
| Workout auto-detection | Cloud endpoint POST /api/activity-tagging/v2 — not available offline |
| Cycle prediction | Proprietary hormonal cycle model; temperature inputs available but predictive model is not |
| Move every hour | Per-hour inactivity alert data is not in the BLE history stream |
Proxy implementations shown in-app
For Resilience and Cardiovascular Age, Protocol Pal shows clearly-labeled proxy cards based on published science — not Oura's model. These are opt-in and only shown when the data source is BLE.
- Resilience proxy: HRV z-score vs 30-day personal baseline → 0–100
- HRV Age proxy: rMSSD vs age-group norms (Shaffer & Ginsberg 2017)
Open-source & scientific references
Protocol & software
- open_oura (MIT) — Oura BLE protocol reverse-engineering, AES-128-ECB auth, packet framing
- open_ring (GPL-3, reference only) — Control-plane BLE commands; not shipped in Protocol Pal
Science
- OSSA 2.0 sleep staging validation, 91.7% 2-stage accuracy — doi:10.1016/j.sleep.2024.01.023
- Shaffer & Ginsberg 2017 — age-group rMSSD norms used in HRV Age proxy
- Jackson-Blair equation — VO₂ max demographic estimate
- Oura Resilience Score inputs — ouraring.com/blog
- NUS Vascular Age validation — ouraring.com/blog