You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PpgHr + decodeWhoop5HistoricalV26 recover HR from the optical PPG waveform for v26 (WHOOP 5/MG). v25 (WHOOP 4.0) carries the same kind of waveform — PostHooks.swift:326 even labels bytes 23–73 PPG waveform (optical) — but it's only marked as a region, never extracted or fed to the estimator. So v25 straps decode motion + timestamp but no HR, even though the machinery to derive it now exists. This looks recoverable.
Evidence
24 real v25 type-47 reject frames from a user's strap log (App 1.93, WHOOP 4.0, resting), across 3 historical sessions. The log's live HR notify lines (realtime type-40/43) are an independent ground truth — they don't come from these historical records.
1. All 3 sessions autocorrelate to the right HR. Concatenating each session's per-second records into a 24 Hz stream and taking the normalised autocorrelation peak:
session (trim)
recovered
live-HR ground truth
conf
70476
60.0 bpm
~60 bpm
0.89
70480
60.0 bpm
~60 bpm
0.75
70484
60.0 bpm
~60 bpm
0.53
2. The samples are i16-LE on an ODD byte grid — a clean alignment signature (even offsets read i16s straddling sample boundaries → garbage):
offset
parity
HRs (3 sessions)
conf
15
odd
[60, 60, 60]
0.84
16
even
[69, 53, 65]
0.12
17
odd
[60, 60, 60]
0.72
18
even
[69, 32, 33]
0.12
19
odd
[60, 60, 60]
0.72
20
even
[131, 32, 44]
0.13
3. The per-record signal is visibly a pulse. One record at odd offset 15, DC-removed (first ~4 samples are header/unix-tail bleed, then a clean rise→fall→rise):
The clean pulse spans roughly bytes 25–66; reading past ~67 corrupts it (the fields before gravity@73). Consistent with — and a refinement of — the existing 23–73 region label.
What's pinned vs. what needs your corpus
Pinned here: feasibility; odd-offset i16 encoding; pulse shape; approximate span ~25–66; the ~24 Hz / one-record-per-second model matching v26.
Needs the v26 rig: exact start/end byte + sample count (21 vs 24) and validation across non-resting HR, multiple straps, motion artifact, using the analyze_v26_waveform.py + live-HR-ground-truth methodology. The evidence here is resting-only, one strap, one 8 s autocorrelation window per session — promising, not conclusive.
Proposed work
Pin the byte-exact v25 waveform layout + sample rate against a real capture corpus (v26 methodology).
Summary
PpgHr+decodeWhoop5HistoricalV26recover HR from the optical PPG waveform for v26 (WHOOP 5/MG). v25 (WHOOP 4.0) carries the same kind of waveform —PostHooks.swift:326even labels bytes 23–73PPG waveform (optical)— but it's only marked as a region, never extracted or fed to the estimator. So v25 straps decode motion + timestamp but no HR, even though the machinery to derive it now exists. This looks recoverable.Evidence
24 real v25
type-47reject frames from a user's strap log (App 1.93, WHOOP 4.0, resting), across 3 historical sessions. The log's liveHR notifylines (realtime type-40/43) are an independent ground truth — they don't come from these historical records.1. All 3 sessions autocorrelate to the right HR. Concatenating each session's per-second records into a 24 Hz stream and taking the normalised autocorrelation peak:
2. The samples are i16-LE on an ODD byte grid — a clean alignment signature (even offsets read i16s straddling sample boundaries → garbage):
[60, 60, 60][69, 53, 65][60, 60, 60][69, 32, 33][60, 60, 60][131, 32, 44]3. The per-record signal is visibly a pulse. One record at odd offset 15, DC-removed (first ~4 samples are header/unix-tail bleed, then a clean rise→fall→rise):
The clean pulse spans roughly bytes 25–66; reading past ~67 corrupts it (the fields before gravity@73). Consistent with — and a refinement of — the existing 23–73 region label.
What's pinned vs. what needs your corpus
analyze_v26_waveform.py+ live-HR-ground-truth methodology. The evidence here is resting-only, one strap, one 8 s autocorrelation window per session — promising, not conclusive.Proposed work
decodeWhoop4HistoricalV25PPG extraction (odd-offset i16 →ppg_waveform) mirroringdecodeWhoop5HistoricalV26.PpgHrlane so v25 yields HR alongside the gravity it already decodes — which also backfills retroactively via the reject-archive replay (Harden the reject-archive replay gate (failure-aware + per-app-version) #152). KeepPpgHrconstants in lockstep across Swift/Android.Repro
Save the dumped
rejected frame[..]hex lines from a v25 strap log tolog.txt, then: