How recovered are you?
Charge
Your overnight autonomic recovery, read from heart-rate variability and resting heart rate against your own rolling baseline.
The science
NOOP turns your strap's raw heart rate, HRV and motion into three daily scores using open, peer-reviewed sport science. Every number is computed on your device, carries an honest confidence tier, and shows nothing at all rather than fake a value. These are independent approximations of published methods. They are not WHOOP's private algorithms, and NOOP is not a medical device.
Before the maths, the protocol
A WHOOP strap does not speak any standard health profile. To read the band you bought, NOOP talks to it directly over its own private Bluetooth Low Energy protocol, locally, never over WHOOP's servers. That protocol was understood through open community reverse-engineering and re-verified on real hardware. This is interoperability with hardware you own, not impersonation: NOOP reads what your device already records and keeps it on your machine.
The boundary, stated plainly
The reverse engineering
Alongside the two standard Bluetooth services every strap advertises, there sits a hidden, vendor-specific GATT service that carries the real physiological data. It only flows after a quiet bonding step. Two community projects mapped it first; NOOP ports their findings and re-verifies the sensor scales and field offsets against a real strap before trusting a single byte.
The custom service exposes four channels: a command write, a command-response notify, an event notify, and the big fragmented data notify that carries the biometric payloads.
Custom service (WHOOP 4.0) 61080001-8d6d-82b8-614a-1c8cb0f8dcc6 • CMD write → command frames • CMD notify ← responses • EVENT notify ← wrist on/off, tap, battery • DATA notify ← fragmented biometric frames
Two standard services act as a sanity check. The standard Heart Rate characteristic (180D / 2A37) streams heart rate and R-R at about 1 Hz without bonding, which makes it NOOP's reliable HR floor while the custom channels supply everything else.
The custom notify channels stay silent until the link is bonded. The discovery was that a single confirmed write is enough to trigger the operating system's just-works bonding. No PIN, no pairing screen.
// one benign confirmed write bonds the link GET_BATTERY_LEVEL → with-response write // then, once: HELLO, SET_CLOCK, GET_CLOCK, // stop the raw flood, read the data range
After bonding, NOOP runs a faithful handshake exactly once, sets the strap clock to UTC, then asks for the on-device history. A wrong-length clock write is acknowledged but never latched, a real bug found and fixed here.
Every custom-channel message is a length-prefixed, double-checksummed frame. A cheap header check guards the length so the reassembler can trust it; a full payload check guards the contents.
0xAA | len u16 | crc8 | type | seq | cmd
| payload… | crc32 LE
crc8 guards the 2 length bytes
crc32 (zlib) guards type+seq+cmd+payload
Bluetooth delivers each frame in MTU-sized fragments, so a reassembler accumulates bytes, finds the 0xAA start, reads the declared length, and only emits a frame once every byte is present and both checksums verify. A bad-checksum frame is dropped, never decoded.
Credit where it is due. The WHOOP 4.0 service, the 0xAA CRC8/CRC32 envelope and the stream layouts come from the community project my-whoop; the WHOOP 5.0 service, the CRC16-Modbus header check and the static client hello come from the community project goose. Where a constant is a direct transcription, the source says so, and sensor scales and offsets are additionally re-verified on real hardware.
Two generations, one decoder
WHOOP 4.0 (codename "Harvard") and WHOOP 5.0 (codename "puffin") differ in just a handful of places: their service UUIDs, their header checksum, where the inner record begins, and how a session starts. Every generation difference is funnelled through a single switch, so everything downstream of the decode is generation-agnostic.
| Aspect | WHOOP 4.0 · Harvard | WHOOP 5.0 · puffin |
|---|---|---|
| GATT service | 61080001… | fd4b0001… |
| Header check | CRC8 over the length bytes | CRC16-Modbus over the header |
| Inner record starts at | byte 4 | byte 8 |
| Payload check | CRC32 (zlib) | CRC32 (zlib), unchanged |
| Session start | confirmed-write bond, then hello | static CLIENT_HELLO frame |
WHOOP 5.0 swapped the cheap length-byte CRC8 for a CRC16-Modbus over the first six bytes of the frame, and moved the inner record from byte 4 to byte 8.
0xAA | 0x01 | len u16 | hdr u16
| crc16-modbus over frame[0..6]
| type | seq | cmd | data… | crc32 LE
poly 0xA001, init 0xFFFF, reflected
The payload CRC32 is identical to 4.0. The whole 4-versus-5 difference is one branch on the device family.
The 5.0 path is confirmed on a real strap. It needs an encrypted link first, so the session bonds before anything else, then mirrors the 4.0 flow on the new transport.
Within one generation a strap may run different firmware with a different record layout. NOOP never assumes one layout transfers to another.
One direction, on your device
The whole pipeline lives on your phone or Mac. Bytes are decoded, checksum-verified and reassembled, written to a local SQLite store, turned into scores by pure on-device analytics, and drawn. Nothing is ever sent off-device, because there is nowhere to send it.
The strap holds about 14 days of history on-device, and NOOP re-offloads it locally about every 15 minutes while connected, scoring each night the strap dumped. A chunk is only forgotten by the strap once NOOP has the data durably stored and has confirmed the acknowledgement, so an interrupted offload resumes exactly where it stopped. The expensive raw sensor flood is switched off on connect to spare Bluetooth airtime and strap battery. No step in this chain touches a server.
Charge, Effort and Rest, each on a 0 to 100 scale, each traceable to its source.
How recovered are you?
Your overnight autonomic recovery, read from heart-rate variability and resting heart rate against your own rolling baseline.
How hard did your heart work?
Your day's cardiovascular load. Easy days sit low, an all-out day approaches 100, and that stays genuinely rare.
How restorative was your sleep?
A composite over each staged night: how long, how efficient, and how settled your heart was through it.
Each score is a pure, deterministic function of your strap's raw streams. Same inputs, same outputs, every time, which is exactly what lets the maths be unit tested against fixed vectors. The display names changed (Recovery to Charge, Strain to Effort, Sleep Performance to Rest) but the stored data keys did not, so years of history keep working.
Inside a score
Every number on this screen was decoded from your own strap, checksum-verified, and scored on your device. Tap any score and it unfolds into the exact terms that built it: each driver, its weight, and how far it sat from your personal baseline. Nothing is hidden behind a brand. If a term is missing, you see that too, and the remaining weights renormalise in front of you.
Score one, in full
Charge is a robust z-score plus logistic composite, led by your heart-rate variability measured against your own rolling baseline. Higher HRV versus baseline means more Charge. These are our weights, openly documented, not WHOOP's private model.
| Driver | Weight | Direction |
|---|---|---|
| HRV vs baseline | 0.55 | higher gives more Charge (dominant) |
| Resting HR vs baseline | 0.20 | lower gives more |
| Rest quality (last night's sleep) | 0.15 | higher gives more |
| Respiration vs baseline | 0.05 | lower gives more |
| Skin-temp deviation | 0.05 | further from baseline gives less |
Every driver is standardised against your personal baseline using a robust z, not a raw mean and standard deviation.
z = (value − mean) / (1.253 · spread)
The 1.253 converts an EWMA mean-absolute-deviation into an approximate Gaussian sigma, since the expected absolute deviation of a normal is about sigma divided by 1.253. For lower-is-better drivers (resting HR, respiration) the z is simply inverted.
The weighted-mean z is squashed onto 0 to 100 with a logistic curve.
score = 100 / (1 + e^(−1.6 · (z + 0.20)))
The slope of 1.6 puts roughly plus or minus two z across the full red-to-green band. The offset anchors a z of zero to about 58 percent, matching the published population-average recovery. Missing terms drop out and the weights renormalise, so a thin day still reads honestly.
Bands: red below 34, yellow 34 to 67, green at or above 67. Resting HR is the lowest sustained floor of the night, taken as the minimum of five-minute non-overlapping bin means so a single dropped beat cannot define it.
Score two, in full
Effort turns every second of heart rate into a training impulse, weights time in harder zones more heavily, then compresses it logarithmically so easy days sit low and an all-out day approaches 100. It is an independent implementation of published exercise physiology, not a reproduction of WHOOP's Day Strain.
Karvonen (1957). Intensity is read as a share of your usable range, not raw bpm.
HRR = HRmax − RHR %HRR = (HR − RHR) / HRR × 100
HRmax comes from the observed 99.5th percentile once there are 600-plus samples, floored by Tanaka (2001): HRmax = 208 minus 0.7 times age.
Each sample contributes a weighted dose of effort, by one of two published methods.
Edwards (1993): zone weight 1–5 × duration at the 50/60/70/80/90 %HRR cut-offs Banister (1991): dur × x · 0.64 · e^(b·x) x = %HRR/100, b = 1.92 (men) / 1.67 (women)
The accumulated impulse is compressed onto 0 to 100.
Effort = 100 · ln(TRIMP + 1) / ln(D), D = 7201
D equals 7201 by design: the Edwards daily ceiling is top zone weight 5 held for a full 24 hours, that is 5 times 1440 equals 7200. So ln(7201) over ln(7201) is exactly 1, mapping that ceiling to precisely 100. The old 0 to 21 scale used the identical denominator, so the rungs never moved. A 100 today is as rare as a 21.0 once was.
A long walk with little cardio still counts: when cardio impulse is low but step and active-energy load is high, Effort is raised to a movement-derived floor. Imported WHOOP Day Strain (0 to 21) is rescaled by 100 over 21 on import, a lossless round trip, so everything on the Effort axis shares one scale.
The log curve is the honest part. Hundreds of impulse units separate a gentle day from a brutal one, but a single number stays legible, and a 100 stays earned.
Score three, in full
Rest is a 0 to 100 composite over each staged night, not a bare efficiency number. It blends four components, weighted toward the one that matters most: did you actually get enough sleep.
| Component | Weight | What it measures |
|---|---|---|
| Duration vs your personal need | 0.50 | how long you slept against your own need (8 h default, refined by your recent average) |
| Efficiency (asleep / in‑bed) | 0.20 | how efficiently you slept once down |
| Restorative share ((deep + REM) / asleep) | 0.20 | how much of the night was restorative |
| Consistency | 0.10 | how regular your sleep and wake timing is |
Rest consumes whatever stages each device can provide (motion on the 4.0, PPG and motion on the 5 and MG as it unlocks). The blend is similar in spirit to a sleep performance percentage, but it is our own, and it feeds straight back into Charge as the rest-quality driver.
Honest by default
Every score carries one of three confidence tiers, so a sparse day reads truthfully instead of faking a number. When NOOP cannot compute a score honestly, it shows nothing at all.
Full inputs present
Enough to show, but thin
Still learning your body
Charge is the strictest, because HRV is its dominant driver and it needs several nights to learn your personal baseline first. Until then it stays in Calibrating, which is more honest than guessing.

Underneath every score
HRV leads Charge, so a single bad beat cannot be allowed to swing it. Before any RMSSD, SDNN or pNN50 is computed, the raw R-R intervals pass through a deterministic three-stage clean, exactly per the Task Force (1996) standard.
The app reports the sample counts before and after cleaning, so you can see the quality of every reading. Honest substitution: the reference pipeline used a Kubios-style classifier that is not available on-device, so NOOP uses the simpler, fully deterministic Malik rule. It does not model missed or extra-beat insertion the way Kubios does, and we say so.
Where the limits are real
Without an EEG, four-class staging from a wrist signal has a known ceiling of about 65 to 73 percent epoch agreement (Walch 2019). We do not pretend otherwise. Light versus deep separation is the weakest link, so deep-minute estimates are the least reliable output we produce.
A gravity-stillness detector builds the night, cross-checked by a citable te Lindert 30-second Cole-Kripke index.
SI = 0.001 · Σ wₐ·Aₐ, asleep iff SI < 1 weights [106, 54, 58, 76, 230, 74, 67]
A run must exceed 60 minutes and be heart-rate confirmed (mean HR within 1.05 of the day's median) before it counts as sleep.
Per 30-second epoch, against session-relative percentiles:
The label sequence is median-smoothed over 5 epochs, then sanity-corrected against what sleep actually does:
Two more figures, each cited, each labelled as the approximation it is.
Men: VO₂max = 100.27 − 0.296·age + 0.226·PA
− 0.369·waist − 0.155·RHR (SEE 5.70)
Women: VO₂max = 74.74 − 0.247·age + 0.198·PA
− 0.259·waist − 0.114·RHR (SEE 5.14)
RHR is a rolling 7-day median. PA is a physical-activity index NOOP reconstructs on-device from the training you actually did, mapped onto the same 0 to 7.5 scale the HUNT questionnaire produced, so the input is honest rather than self-reported.
The model's standard error of estimate is roughly 5 ml/kg/min on the VO₂max itself. That is large, so the number is a direction of travel over weeks, not a lab measurement. We respect that in the UI:
The same maths, in your hand
The scoring lives in a small, deterministic, open package. Here is what it looks like once it reaches the app.




Every calculation runs on your phone or Mac. Your heart rate, your HRV, your sleep. None of it is uploaded, because there is nowhere to upload it to.
End to end
The whole pipeline lives on your device. The recompute engine runs about every 15 minutes while connected, scoring each night the strap offloaded. Where a WHOOP export already covers a day, that export still wins.
Where we are honest
The spine of the project
Every calculation on this page runs where the data is captured, on your own device. There is no account to create, no analytics, no tracking, no ads. Your most personal data, your heart and your sleep, never leaves your phone or Mac. You own it, and you can export it.
Open and local by design, so the maths can be read, run and tested, and can outlive any one company.
The quieter case
Wearables are typically obsolete in two to three years, glued shut and hard to repair, and replacement is driven by subscriptions and planned obsolescence rather than failure. About 30 percent of fitness trackers and smartwatches are abandoned. When you cancel a subscription, the band you bought can quietly become e-waste.
NOOP keeps the strap you already bought useful for years, with no forced upgrade, no account, and no subscription that can brick it. On-device means no data-centre energy is spent per reading. Honest framing: NOOP is not a fix for the e-waste problem, it just refuses to add to it.
E-waste figures: UN / ITU / UNITAR Global E-waste Monitor 2024, ewastemonitor.info.
The full, source-linked write-up lives in the project wiki under Analytics, Architecture and BLE Reverse Engineering, with the maths in the open StrandAnalytics package and the protocol decode in WhoopProtocol. Read it on the repo. Questions or corrections, write to thenoopapp@gmail.com.