NOOP Staging — base 12.1.0 · 2026-10-13 · 3a0b74b
Rolling latest testing build — refreshed in place, so this tag is always the newest. NOOP main + ryanbr's open PRs + vetted community PRs, built with the NOOP Staging identity. Android and macOS install beside the official app; the iOS ipa carries the production bundle id, so it replaces an installed official app and uses its data. Filenames carry the base app version (v12.1.0).
- Android (release) —
NOOP-android-v12.1.0.apk— optimized, non-debuggable build; installs beside the official app.com.noop.whoop.staging. - Android (debug) —
NOOP-android-debug-v12.1.0.apk— debuggable build.com.noop.whoop.debug. - macOS —
NOOP-macos-v12.1.0.zip— unzip, then right-click → Open (ad-hoc signed, not notarized; Gatekeeper warns once). If it says "damaged", runxattr -dr com.apple.quarantine /Applications/NOOP.app. - iOS —
NOOP-ios-unsigned-v12.1.0.ipa— sideload via AltStore / Sideloadly (re-signs on-device). Replaces an installed official app (production bundle id, shared data), so back up first if you have one. Embedded watch app stripped, widget extension retained for Home/Lock-Screen widgets and Live Activities.
Base app version 12.1.0 · built 2026-10-13 · 3a0b74b.
NOOP 12.0.0
NOOP v12.0.0
A major release about choice and about honesty. Today can be drawn two different ways, a WHOOP 4.0 can
hand its heart rate to any app on your phone, daytime stress is measured against your own baseline
rather than a population's, and NOOP speaks a tenth language. Behind that, a long run of readouts that
had been claiming slightly more than they could attribute now say only what they know.
Today, drawn your way
- Ring gauges or liquid vessels (#2311, #2320, thanks @andigandhi). Today's gauges can be rings or
the liquid vessels, chosen per device, on Android and on Apple. Reduce Motion is honoured in both. - The hero ring, cleaned up (#2410, #2374, #2421, thanks @mailingjash). It draws flat rather than
through a bloom stand-in, its label sits centred on the ring rather than beside it, and the active
strap's own battery appears in the Liquid header. - Nothing redraws while nothing moves (#2444, thanks @UtkuDenizAltiok), so the sky and the rings
stop re-rendering between ticks. - A Today-only Key Metrics view (#2601, thanks @kavemang), and the Key Metrics header no longer
names a window it is not drawing (#2377, thanks @andremiliano). - Swipe the day the same way on both platforms (#2379, thanks @andremiliano).
Your strap's heart rate, in any app
- Direct Bluetooth heart-rate broadcast on WHOOP 4.0 (#2400, thanks @don86nl). A 4.0 can advertise
the standard heart-rate profile, so another app on your phone reads it live. The Today row says
Partly rather than Yes, because the 4.0 offers the broadcast and not the rest of what a strap does. - A link that reports its shape (#2399, #2429, #2436, #2411). The epitaph carries the signal shape
rather than only its last reading, a false start-of-frame is rejected by its own header checksum, the
peripheral is asked for again once the radio is up instead of scanning for one already bonded, and an
OS-level strap link is adopted from either WHOOP family. - A strap last seen low that has not been heard from since now says so, rather than going quiet.
Daytime stress, measured against you
- A personal daytime lens (#2125, #2430, #2431, #2432, #2452, thanks @bartmuskala). Resolved once a
day instead of once per surface and anchored to real local days. The line ramps down the chart rather
than across the day, and hours masked by activity now say why they are masked. - Today honours the same baseline the detail screen does (#2450, #2504, thanks @kavemang), and it
loads faster. It stays opt-in and off until you ask for it. - The LF/HF ratio prints through the same arithmetic on both platforms (#2369, thanks @ayiskakov).
ECG on WHOOP MG
- The R17 layout corrected, along with its wrist values and its START list.
- Android drives the turn-on probe Apple already had, an in-flight offload is cleared before a
realtime trace is requested, and the capture-may-be-running latch survives an app restart and reaches
the Devices card.
Language
- Italian (#2454). NOOP's tenth language.
- Russian that counts properly. Grammatical plurals across the app, the Watch complications and the
Apple catalogues, so counts take the right form instead of always one. Eight mistranslated Android
strings are corrected, and the copy a widened scanner surfaced is localised. - The coverage gate is diff-scoped again (#524, thanks @digitalerdude), and a push is compared
against the tip before it rather than against itself (#2329).
Sleep
- A deep base prior of 0.15, on PSG evidence (#2576, thanks @UtkuDenizAltiok), scored subject by
subject rather than in aggregate. - The hypnogram read-out matches its own rows, in order and in value, and stage rows align with
chart depth. - Two sources recording one night are collapsed (#2550, thanks @kavemang), and the dedup heal's
bank-recency witness is handed to the computed id alone (#2372).
Oura
- A ring that keeps its place (#2442, #2455, #2457, #2433, #2443, thanks @pipiche38). A sleep-window
stash survives a reconnect, an interrupted drain banks its resume cursor so the next connect does not
store again what it already holds, and a night the drain served twice is readable. - A hypnogram tail that is padding, not time (#2564, thanks @pipiche38), so a pass no longer shifts
early, and the ring's final pass no longer loses to an older one. - A read crossing wake keeps the daytime beats (#2624, thanks @pipiche38), chosen per hour so a
day-wide read does not swap channels. - Experimental all-day heart rate and HRV holds its daytime arming for the learned night rather than
for every screen-off (#2412).
Charge
- A breakdown that agrees with its headline (#2466, thanks @andremiliano). It reads the headline's
own baselines rather than resolving a second set, and there is now a written rule for which nights a
Charge baseline reads. - An old WHOOP import no longer anchors the baselines (#2666, thanks @DX23876).
A quicker Apple app
- Ten performance changes (#2633, #2634, #2635, #2636, #2637, #2639, #2640, #2641, #2642, thanks
@AlexSchmidt1999). Live Today Effort computes off the main actor, resolved Trends windows are reused
across unrelated publications, HealthKit observer deltas are bounded to the recent sync window, shared
chart styles resolve once per content update, full-resolution chart selection uses a date-ordered
lookup, and the shared panel background composites fewer layers. - Re-score passes that behave in the background (#2612, #2616, thanks @UtkuDenizAltiok and
@kavemang), spaced at least thirty minutes apart and claiming their pass before a store await.
Shortcuts, Health and the Lock Screen
- A recorded GPS route through Shortcuts (#2340, thanks @rodrigosa7), as GPX or FIT, and the picker
says what a FIT leaves out. - Apple Health export names its chart heart-rate sources and refreshes write authorization.
- Android reads your profile weight from Health Connect (thanks @paradix86).
- One Lock Screen switch per thing NOOP shows, each touching only its own.
Smaller corrections
- Android honours your selected accent in Material controls (#2627) and rejects a malformed custom
RGB value. - Vital dates carry their weekday (#2622, thanks @kavemang).
- Today explains when Start session is available, and the Sleep movement strip says its scale is
per-night. - Deep Timeline zoom and pan stop snapping back (#2368, #2382, thanks @andigandhi), with x-axis
labels and per-minute zoom ticks on both platforms. - Saved-workout overlap suppression is measured rather than guessed at (#2527, thanks @kavemang),
ahead of choosing a duration-aware threshold.
Still open
WHOOP 5/MG history remains unsolved, and the cause is now established rather than suspected: the strap
refuses the pairing handshake outright, so it is never given a clock, and an un-clocked 5/MG does not
persist sensor data to flash. Its offloads complete empty. Live heart rate works; the backfill cannot,
until the handshake does.
What's Changed
- feat(rescore): log a pass's CPU time and assertion expiries beside its elapsed time by @Iskrata in #2297
- fix(oura): watch the auth handshake — an unanswered get_nonce is retried, toggled, then dropped, never left "Connected" by @pipiche38 in #2307
- fix(live): no WHOOP strap picker or Scan / Re-scan under an active Oura ring — a ring gets its status and a Reconnect by @pipiche38 in #2306
- docs(oura): stop claiming an Apple framework property as a twin in #2316
- fix(charge): score the breakdown against the baseline the ring rides in #2317
- fix(rescore): pace a backgrounded pass in 10-second quanta so it isn't suspended after every night by @Iskrata in #2318
- Android: Change LiquidVessel gauges to GlowRing by @andigandhi in #2311
- Android: let Today choose ring gauges or the liquid vessels in #2320
- Alarm readback: refuse to judge one that answered an earlier arm in #2322
- Sleep: show "May be incomplete" only when the night actually reads short in #2324
- Parity: re-derive the twin-map authority after the scanner and source drift in #2325
- Parity: declare the OuraAuthWatchdog twin where the ledger reads it in #2326
- Changelog: say which platforms the 11.8.0 lift log actually shipped on in #2328
- i18n CI: make the coverage gate diff-scoped again, not a full-tree invariant by @digitalerdude in #524
- i18n CI: diff a push against the tip before it, not against itself in #2329
- docs: WHOOP 4 protocol parity, contract-to-implementation map and source-reference check by @bhelm in #2310
- Sleep: ask every block whether the night was staged on sparse motion in #2331
- Sleep: give Android the H9 low-confidence note it was promised in #2330
- BLE: put the link's signal strength on its epitaph in #2333
- Health: say why HRV is blank when the night over-reported R-R in #2336
- feat(apple): dynamic charge ring color based on recovery score zones by @adamstefanik in #2309
- BLE: redact the chosen strap name in the rename log in #2337
- Android: Show Effort Suggestion on Today Page by @andigandhi in #23...
NOOP 11.8.0
NOOP v11.8.0
A release about doing more on the strap itself, and about turning off the parts you do not want.
A gym log book, advanced from your wrist
NOOP now carries an on-device lift log. Build a session, and move between sets with a double-tap on the
strap rather than picking the phone up mid-set. It is stored in a new schema with a matching Room twin
on Android, and the set metrics are computed by the same engine on both platforms rather than by two
that can drift apart.
A Coach that switches off
The AI Coach now has a single master switch. Turning it off retires the tab, the generated brief, the
home-screen widget and the tray notification, and it stops every request that would otherwise leave the
device. This is the whole feature going quiet, not just the screen being hidden.
Its settings moved onto their own screen rather than sitting in a crowded list. Android gained the
bottom-bar tab that iPhone already had, Disconnect is now somewhere an iPhone can reach, the current
OpenAI models are offered with the catalogue kept up to date, and a rate-limited request shows what the
provider actually said instead of a generic failure.
Sync the strap from a shortcut
A Sync Strap App Intent runs a sync from Shortcuts, the Lock Screen or a Home Screen button. A Live
Activity shows the offload while it runs, and an optional setting keeps the screen awake for as long as
it takes.
Reconnecting when the app is off-screen changed underneath that. iOS throttles background scanning hard
enough that a strap could sit undiscovered for minutes and then appear seconds after the app came to the
foreground. NOOP now asks for the last strap by name in the background, which iOS does honour, and keeps
scanning in the foreground so a strap you have switched to is still found.
Workouts that are easier to keep tidy
Sessions shorter than a minute are discarded rather than cluttering the list, and the list itself splits
into Current and Archived so a long history stays navigable. Deleting a workout now clears it from every
namespace the list reads, including the copy Apple Health had kept after the store let go of it.
The WHOOP sports that were missing are selectable, and the icons those exposed are fixed.
Auto-detection no longer saves anything on its own: it asks, and waits.
Readings that admit when they failed
A failed WHOOP 5 flag read was being reported as though it were a value. It now says it failed, on both
platforms. The frame integrity verdict reaches every consumer intact rather than being narrowed on the
way through, and the strap family NOOP records is the one the connection actually established rather
than the one it initially picked.
On Oura, a notification that tiles exactly into several packets is now read to the end instead of
stopping at the first. A young ring no longer adopts an anchor unit ten times too large, settled by
adjacency rather than assumption, and a diagnostic reports which ring epoch each sidecar row believes
in.
Charts that stop redrawing everything
Workouts and Sleep were re-evaluating their entire screens on every live heart-rate tick, roughly once a
second whenever a strap was connected. They no longer do. Three charts that had no hover gained a
tooltip, two uncapped series are downsampled before drawing, the sleep motion trace computes its peak
once rather than per epoch, and the Apple stress level rounds the way Android's already did.
The steps charts are clearer, with an optional 30-day average card.
Apple Health that keeps up
A re-score that was deferred now publishes to Apple Health once it lands, rather than being missed
because the publish already ran. Workouts are observed for live delivery, and a pass running in the
background paces itself under the CPU limit iOS enforces instead of being killed by it. A CSV import no
longer turns a correct SDNN into RMSSD on the way back out.
Localization
Chinese is complete in both scripts, across the app and the watch complications. The French recovery and
strain words are repaired, and the theme picker says light rather than lightweight.
A gap that had been letting UI copy ship English in every locale is closed. The scan that checks whether
a string has a catalogue entry was not looking at the spelling the codebase tells you to prefer, so
nothing reported strings that had none. Closing it surfaced the Home screen's own labels and the terms
you agree to on first launch, both of which are now translated.
What's Changed
- Add the v46 schema for an in-app strength log, with its Room twin by @UtkuDenizAltiok in #2098
- coach(ios): put Disconnect where iPhone can reach it in #2207
- doc(issues): let the bug form name an Oura ring, not only a WHOOP strap by @pipiche38 in #2209
- health(ios): delete the workouts Apple Health kept and the store did not in #2213
- battery: say whose charge it is on all seven surfaces, not three in #2216
- nav(android): give Coach the bottom-bar tab it has on iOS in #2222
- Repair parity governance maintenance flow by @bhelm in #2212
- doc(issues): point an Oura reporter at the ring's known limits, as the 5.0/MG line does for straps by @pipiche38 in #2215
- fix(workouts): make auto-detection confirmation-only by @tigercraft4 in #2220
- fix: do not report a failed WHOOP 5 flag read as a value by @Trillient in #2193
- whoop5: guard the failed flag read on every platform, and on Android too in #2223
- docs: expand protocol coverage and clarify WHOOP 4/5 compatibility by @bhelm in #2183
- test(sleep): pin the night-caption label and date pairing (#2199) in #2224
- Lift Log: an on-device gym log book, advanced by a double-tap on the strap by @UtkuDenizAltiok in #2099
- i18n(zh): complete the Android locale and carry it to the Apple catalogue in #2226
- i18n(watch): translate the complications into both Chinese scripts, and build them in #2227
- i18n(apple): complete zh-Hans in the main catalogue, and take zh-Hant to 79 in #2228
- Test real Room upgrades against the released schema by @kavemang in #2225
- tools(parity): make a one-sided property declarable in #2231
- analytics(android): Kotlin LiftMetrics twin of the Swift engine (#2099) in #2232
- tools(parity): give the stale-base dead end an exit, then take it in #2233
- parity_ledger: keep trailing hex digits f/d when stripping literal suffixes by @bhelm in #2221
- fix(sleep): name a night by the evening it belongs to, not its onset date in #2235
- i18n(design): complete Chinese, and stop asking anyone to translate previews in #2244
- i18n(design): repair the French recovery and strain state words in #2247
- i18n: make the theme picker say light, not lightweight, on both platforms in #2249
- i18n: drop two dead keys, and gate format specifiers in every locale in #2250
- Fix Coach rate-limit error details by @kavemang in #2245
- fix(rescore): let a resumed pass stand down when nothing changed in #2251
- fix(oura): settle the 0x13 anchor unit by adjacency so a young ring never adopts ticks×10 by @pipiche38 in #2240
- diag(oura): report which ring epoch each sidecar row believes in in #2253
- feat(coach): move the coach settings onto their own screen in #2254
- Propagate the full frame integrity verdict to every consumer by @bhelm in #1985
- feat(coach): make the live model catalogue reachable, and keep it current in #2256
- feat(coach): offer the current OpenAI models in #2255
- fix(oura): read every packet of a notification that tiles exactly into several by @pipiche38 in #2237
- test(oura): name the framing test hex helper so it cannot alias a production one in #2257
- doc(readme): say which ring each Oura row was measured on, and update the SpO₂ and ring-only-day rows by @pipiche38 in #2236
- Align Apple stress-level rounding with Android by @kavemang in #2258
- tools(parity): stop the scan walking into build output in #2259
- feat(workouts): offer the WHOOP sports NOOP was missing, and fix the icons they exposed in #2260
- stress: take the Apple screen's work off the main actor too in #2263
- Persist the family the strap actually established by @kavemang in #2230
- fix: stop the half-open range operator erasing a callsite in the parity scan in #2267
- feat(coach): offer a master switch that turns the AI Coach off in #2269
- fix(health): stop a CSV import turning a correct SDNN into RMSSD on export in #2271
- docs: share agent guidance throu...
NOOP 11.7.0
NOOP v11.7.0
A release about the app keeping up with you, and about readings meaning what they say.
A stress screen that keeps up
Scoring a day of stress reads hundreds of thousands of samples. That work was happening on the same
thread that draws the screen, so Today and Stress could sit unresponsive while they loaded. It now
runs off that thread on both platforms, the baseline is reduced a day at a time rather than held a
month at once, and the unprompted rescore yields to whatever you are doing instead of competing with
it for the machine.
The home-screen widget fills on its own schedule without the app being opened. A rescore that produces
nothing is retried rather than being allowed to spend the whole refresh interval, and the Today curve
is kept as fresh as the screen it links to.
The readings themselves now agree with each other. The 0 to 3 level has one spelling across the widget
and the card, and is no longer rounded a second time on its way to the widget. The screen draws its
line from the same sliding read the number is taken from. Movement is drawn as the stretches it
actually covers rather than as dots on the axis. An early morning says the day has not started rather
than reporting Calibrating, which reads like something is wrong.
WHOOP 5 readings in the units the strap sends
Thanks to @Trillient, two decoding faults are corrected.
R-R intervals arriving on the standard heart-rate profile were converted as though they were in the
Bluetooth spec's units. A WHOOP 5 sends milliseconds directly, so every interval taken from that
profile came out 2.3% low, and HRV computed from it inherited the error. What settled it was a paired
capture: the same beats arriving on both the strap's own channel and the standard one carry
byte-identical values, so only one of the two conversions could be right.
The console log's record header was read as a 16-bit counter spanning two bytes. Only the first is a
counter, and it wraps at 255. Across 2,978 records from a single night the second byte never moved
through nine of those wraps, which a real counter could not do. Both platforms now decode the two
fields separately.
An Oura ring that stops repeating itself
Thanks to @pipiche38 for a sustained run of corrections to the ring's history path.
A replayed burst that the ring's own clock disproves as a reboot no longer resets the history resume
cursor, and no longer announces a reboot that did not happen. A night that has just been offloaded is
scored immediately rather than waiting for the next periodic tick. History is fetched every five
minutes rather than every fifteen, the anchor check judges a ten second gap rather than only a thirty
second one, and an onset rekey is refused when the assembler's own clip never bound.
There is also a feature-mode control and a consolidated set of Oura diagnostics in the Test Centre,
the raw night sidecar stays in the export, and the SpO2 sample keeps only the channel that carries a
reading.
Scores that survive a re-score
Thanks to @bhelm. WHOOP 5 HRV and Recovery, and the vitals derived from R-R, were being dropped by a
re-score pass that could not recompute them, so a value you already had could disappear on a pass that
had nothing better to put in its place. Both are now preserved.
Readings that say when they are at a limit
A Fitness Age resting at the end of its scale now says so, rather than presenting a bound as though it
were a measurement, and points at the number that still moves when the age stops. On Sleep, a night
already detected and staged no longer sits under a banner claiming it is still being calculated, and
browsing back to a night with no stage data no longer shows the most recent night's date beneath it.
Diagnostics that name the cause
A device reporting no R-R now says whether the beats were never banked or were refused by the unit
policy, which are different problems with different answers. The nightly HRV summary says why it
reported nothing. A retired probe tells whoever reads its output which switch to turn on, and stops
under-reporting its own retirement. A re-arm clears a refusal latch that previously could not be
reached once set. The sync chip surfaces the strap's backlog.
Thanks
Thanks to @pipiche38 (Oura history, replay judgement, diagnostics and export), @Trillient (WHOOP 5
R-R units and console sequence decoding), @bhelm (preserving legacy HRV, Recovery and R-R vitals),
@kavemang (tile values that shrink rather than truncate, per-signal illness baselines, Android
guards), and @digitalerdude (parity tooling on macOS, sync chip backlog, and documentation that
describes what the code returns).
What's Changed
- fix(android): skip stress scoring without a widget by @kavemang in #2111
- fix(android): guard sleep session cache upserts by @kavemang in #2112
- feat(oura): add a SetFeatureMode enable control, consolidate Oura Test Centre diagnostics by @pipiche38 in #2105
- today(android): move Customize into the header cluster, where it stops covering the wordmark in #2113
- today(android): shape a hosted card clip from what it actually renders in #2114
- stress: reduce each baseline day as it is read, instead of holding thirty at once in #2116
- diag: say whether a device R-R beats were never banked or were refused by the unit policy in #2118
- Preserve legacy WHOOP 5 HRV and Recovery across re-score by @bhelm in #2115
- widget: retry a stress rescore that produced nothing, instead of spending the interval on it in #2121
- fix(oura): don't reset the history resume cursor to 0 on a stale replay the ring's own clock disproves as a reboot by @pipiche38 in #2100
- sleep: a night already in hand silences the calculating banner in #2122
- stress: draw the movement marks as the stretches they are, not as dots on the axis in #2124
- hrv: say why the nightly summary reported nothing in #2129
- illness: a handful of stored nights is not a baseline in #2132
- Describe what DayOwnerSource.candidatePriorities actually returns (#2026) by @digitalerdude in #2133
- probe: let a re-arm clear the refusal latch it could never reach in #2136
- Preserve legacy RR-derived vitals and remove accidental scaffolding by @bhelm in #2141
- ci: let the parity gate see main, so drift stops ambushing other PRs in #2142
- Sync chip: surface the strap backlog, with both counts inflected (#833) by @digitalerdude in #2138
- Say that the day-owner path uses archived candidates, not just inherits them (#2026) by @digitalerdude in #2134
- ble: tell a retired probe reader to turn the switch ON, and stop under-reporting retirement in #2160
- parity tests: pass on macOS, and resolve expand_twin_map's root (#2143 follow-up) by @digitalerdude in #2159
- parity_ledger: resolve the temp checkout root before deriving from it (#2143) by @digitalerdude in #2158
- fix(diagnostics): keep the night's raw Oura sidecar in the export — session-scoped, ring-buffered, fairly capped, generations merged by @pipiche38 in #2154
- doc(oura): correct the macOS pairing limitation - a factory-reset ring works fine by @pipiche38 in #2149
- docs(oura): fix the §7.5 heading left stale by 450256a by @pipiche38 in #2150
- fix(oura): score the ring's freshly-offloaded night now, not at the next periodic tick by @pipiche38 in #2147
- fix(oura): refuse the #1284 onset rekey when the assembler's own clip never bound by @pipiche38 in #2146
- fix(oura): only persist the SpO2 raw channel, drop dc_raw from spo2Sample by @pipiche38 in #2152
- fix(oura): fetch ring history every 5 min, not 15 — the periodic fetch is the real data cadence by @pipiche38 in #2153
- fix(oura): let the #2097 anchor check judge a 10 s gap, not only 30 s by @pipiche38 in #2155
- health: trust Android baselines per signal by @kavemang in #2157
- stress: one spelling for the 0-3 level, on the widget and the card in #2165
- stress: stop the widget snapshot rounding the level a second time in #2168
- stress: give the marker tile's reading the width the chip was holding in #2172
- ci: do not let a failed release delete look like a tag that already exists in #2175
- health: say when a Fitness Age is sitting on the end of its scale in #2176
- stress: keep the Today curve as fresh as the screen it links to in #2177
- today: put scan and connect on the Today header in #2178
- today: show the scan control only while the strap is away, and make it look like a scan in #2180
- stress: do not say Calibrating when the day has simply not started yet in #2182
- widget: refresh the stress curve without waiting for ...
NOOP 11.6.0
NOOP v11.6.0
A release about putting the day's readings where you actually look, and about being honest when a
reading is missing rather than drawing over the hole.
A Today screen you arrange yourself
Today's stress curve and the Trends charts are no longer locked to their own tabs. Both can be hosted
on Today as cards, on Apple and on Android, so the screen you open first carries the things you check
first. A hosted card opens the tab it came from when you tap it, so it is a shortcut rather than a
dead end, and on the classic Today a hero ring now opens its own detail.
There is also a stress widget on both platforms, showing today's hourly curve on the home screen. It
fills on its own schedule without the app being opened, and when there is nothing to show it explains
why instead of leaving you looking at an empty box.
Charts that break where the strap did
A heart-rate line used to draw straight across hours the strap never recorded, which quietly asserted
a reading that was never taken. Those runs are now broken wherever the gap is real, on every surface
that draws a heart-rate trace rather than only the one where it was first noticed.
WHOOP 5 readings corrected
Thanks to @Trillient, R-R intervals from a WHOOP 5 are now read in the units the strap actually sends,
which is the input HRV is computed from. Skin temperature now reaches recovery scoring before the
score is computed rather than arriving after it. Both platforms carry regression tests covering the
gaps these were found in.
Backups that check themselves
An export now verifies the file it actually wrote, on every path that writes one, so a backup that
was truncated is caught at the moment it is made instead of the moment you need it. When the database
itself reports a problem, the full text of what SQLite said is shown in something that can hold it and
copied, rather than clipped to a line that leaves out the part that mattered.
A coach that holds on to your conversation
A rejected API key can be corrected in place without losing the conversation you were in. The coach is
told what the workout was rather than only that one happened, the consent screen describes what it
actually sends, and the morning brief now reaches the screen it was generated for.
Diagnostics that say why
A night that missed the re-score cache now names the setting that dropped it, instead of reporting
only how many nights missed. A dropped link is always recorded, the Live Console reads the device you
selected rather than whichever field happened to be populated, and the Rest card says why it is
waiting on a sync instead of changing in silence.
Oura logs keep your serial out of them: serials are masked in the redactors on both platforms, and
product-info replies no longer reach the raw diagnostics sidecar. Both from @pipiche38.
The Test Centre now hands you its bundle through the same share sheet the strap log uses and stops
there, rather than steering you into opening an issue.
Under the hood
Cross-platform parity now has a scanner, a reviewed authority and a baseline, contributed by @bhelm.
It inventories the declarations and constants that are supposed to match between the Swift and Kotlin
implementations and records the current accepted debt, so that regeneration cannot quietly accept
more. It does not yet gate product changes.
Thanks
Thanks to @Trillient (WHOOP 5 R-R units, skin temperature ordering, regression tests), @pipiche38
(Oura serial masking and diagnostics sidecar), @bhelm (parity governance), @kiesstein (realtime heart
rate on the Breathe screen), @kavemang (readable workout actions), and @UtkuDenizAltiok (duplicate
string key and Info.plist drift).
What's Changed
- The Today heart-rate Min and Max read the samples, not the mean curve in #2038
- Stop the Sleep screen asking the store once per night in #2039
- Stress widget: today's hourly curve on the home screen (Android) in #2044
- Stress widget: the iOS twin in #2045
- Read the stress hour every 30 minutes instead of every hour in #2048
- Host today's stress curve on the Today screen (Android) in #2047
- Name the hosted stress card for what it is in #2049
- Offer the Trends charts as Today cards in #2051
- Make Apple's hosted cards open the tab they came from in #2052
- Host the stress curve on Apple's Today in #2053
- Host the Trends charts on Apple's Today in #2054
- fix: correct WHOOP 5 R-R interval units by @Trillient in #2046
- fix: include skin temperature before recovery scoring by @Trillient in #2042
- Keep the joining space out of the string resources, so concatenated copy reads correctly in #2043
- Android: keep workout actions readable over the daytime scene by @kavemang in #2050
- Arm the realtime HR stream while the Breathe screen is visible by @kiesstein in #2036
- Let a rejected API key be corrected without losing the conversation in #2058
- test: cover WHOOP 5 gap regressions on both platforms by @Trillient in #2056
- Fix a silently-dead duplicate string key, and regenerate the drifted Info.plist by @UtkuDenizAltiok in #2029
- Let a hero ring on the classic Today open its own detail in #2060
- strength: exercise to muscle attribution (stage 1) in #1968
- hevy: workout import, the parsing half in #1967
- Tell the coach what the workout was, not just that there was one in #2061
- Say what the coach consent actually sends in #2063
- Never let a dropped link go unrecorded in #2065
- Break the heart-rate line where the strap recorded nothing in #2064
- android: seed the strap picker from the recorded family, not a hardcoded WHOOP4 in #2067
- Enter a manual workout by its start and end, not only a duration in #2070
- Fill the stress widget without needing the app opened, and say why it is blank in #2074
- Read the Live Console out of the active device, not whichever field is populated in #2076
- Say why the Rest card is pending sync, instead of changing in silence in #2077
- Make two export headers say what they actually mean in #2078
- Say why a night missed the re-score cache, not just how many did in #2080
- Break the heart rate line where the strap recorded nothing, on every surface that draws one in #2083
- Show what SQLite actually complained about, in something that can hold it in #2084
- Verify the backup file that was actually written, on every export path in #2086
- Let the daily brief reach the screen it was generated for in #2088
- Name the config field that dropped the day cache in #2094
- fix(oura): never write GetProductInfo replies into the raw diagnostics sidecar by @pipiche38 in #2090
- fix(oura): mask an Oura serial in the log redactors, both platforms by @pipiche38 in #2095
- Share the Test Centre bundle and stop, instead of opening a GitHub issue in #2096
- Add fail-closed cross-platform parity governance by @bhelm in #1534
New Contributors
- @Trillient made their first contribution in #2046
- @kiesstein made their first contribution in #2036
- @UtkuDenizAltiok made their first contribution in #2029
Full Changelog: v11.5.0...v11.6.0
Downloads — NOOP Staging identity, installs beside the official app (separate data):
- Android —
NOOP-android-v11.6.0.apk(com.noop.whoop.staging) - macOS —
NOOP-macos-v11.6.0.zip(unzip, then right-click → Open) - iOS —
NOOP-ios-unsigned-v11.6.0.ipa(AltStore / Sideloadly)
NOOP 11.5.0
NOOP v11.5.0
A release about NOOP holding a thread: in the coach, in the charts, and in what the strap log is willing
to claim.
A coach that keeps the thread
The coach was a single question and a single answer. It now behaves like something you can return to.
Replies stream in as they are written, so a long answer starts being readable immediately instead of
appearing all at once at the end. The conversation persists between sessions, so you can put the phone
down and pick the thread back up. It can speak its answers, and take a question by voice. A morning brief
sums up the night behind you without being asked.
There are home-screen widgets and Siri shortcuts for the things worth reaching in one step, and an
optional Today launcher card, requested in #1862 by @kggreco11, which stays off until you turn it on.
Your heart rate on the home screen
A widget showing live bpm with the recent trace behind it, on both platforms. It lets the launcher decide
the layout rather than assuming a size, so it sits properly wherever you put it.
Charts that claim less
Four things were wrong at once, and each made the others harder to see.
Points were spaced evenly regardless of date, so a fortnight you did not wear the strap looked the same
as two consecutive nights. Points are now spaced by date, and a gap takes the width it spans.
Every metric was drawn as though it were maximally volatile, which flattened the ones that genuinely move
and exaggerated the ones that do not. Scales are anchored per metric now.
Daily scores were drawn as lines. A line between two daily scores asserts the value travelled through
everything between them, and for a score computed once a day that is simply false. On a series that
swings twenty points overnight it is most of what reads as noise. They are bars now, and a day with no
reading is an empty slot of the right width rather than a plunge to zero.
And the Line and Bars setting in Settings only ever reached the Trends tab. It now applies to the detail
chart you reach by tapping a ring on Today, which is where most people were looking at it.
Alongside that, HRV and Resting HR carry a dashed rule at your own baseline. An absolute HRV number says
little on its own: 42 only means something once you know your normal is 54. The rule is drawn from the
same fold the vitals grid bands against, so the chart and the tile beside it cannot disagree, and it stays
hidden until the baseline has enough nights to be worth trusting.
Skin temperature, as a temperature
Skin temperature was shown as a deviation from baseline, which is the right number for spotting illness
and the wrong one for answering what your temperature is. It now leads with the measured value, with a
Settings choice if you prefer the difference, and the preference applies across the window rather than
only the newest night. Reported in #622 by @bartmuskala.
A Fahrenheit import used to be stored as though it were Celsius. It is converted on the way in now, which
was #111 from @whisp0.
Sleep
A night that the detector chopped into fragments, each too short to count on its own, scored as no night
at all. Those fragments are bridged back into the night they were, reported in #1937 by @bartmuskala.
The filled hypnogram can be dragged to read the clock time under your finger, asked for in #1855 by
@AussieFries. Its axis no longer mislabels a partial timeline as beginning at onset.
A night measured from heart rate alone now reports the HRV it actually measured rather than nothing, and
the Sleep tab stamps a carried value with the day it came from, so an older night can no longer read as
yesterday.
Steps and the day boundary
Step counting and the day boundary now follow your own sleep rather than the clock, so a late night stops
splitting one day's activity across two. The steps calibration also says which half of its input is
missing: asking for phone-step days is no help when what is absent is strap movement.
A wake time for each day
Weekday and weekend wake times can differ. The per-day override drives the strap alarm and the backup
notification too, not only the phone alarm.
Re-scores that take a fraction of the time
Re-scoring re-read sixty days of movement on every pass to fit one steps coefficient, whether or not any
of it had changed, and threw the answer away on every restart. It now re-folds only the days whose
movement actually moved, and keeps what it learned across launches. On a worn library that phase dropped
from roughly thirty-three seconds to under three. Reported in #1538 by @justinjor-bit.
The per-day work behind it is measured now rather than estimated, so the next thing worth speeding up can
be chosen from a number instead of a guess.
The strap log stops guessing
A connection your own phone was holding could be reported as the strap refusing to pair, which sent
people chasing a pairing problem that did not exist. Reported in #1997 by @semoi.
A non-active strap could be scanned, connected and drained while the one you selected sat idle. The strap
you chose is the one that gets connected and credited, from #1881 by @pipiche38.
A bond that ends now records why, in wording that reads the same in every language, and a disconnect
records how long the link held and whether anything was banked. Serial numbers are masked wherever a log
might be shared, including the hex-hidden ones.
Languages
Russian joins the supported locales. The German copy has been converted from the formal Sie to the
informal du throughout. A Portuguese string that a find and replace had corrupted is correct again.
Smaller things
Routes import from Apple Health and Health Connect (#1205). The battery pack reports its own charge
without overriding the strap's gauge, from #1935 by @Zebsi235. Body measurements and exercise distance
carry separate unit preferences, asked for in #1913 by @kavemang. The readings table prints each unit
once, reported in #1942 by @Geg0r. Rest says it is provisional while history is still syncing rather than
hiding itself. Tapping a hero ring on Today opens that metric's detail. The bottom bar can overlay the
content and hide itself as you scroll, on both platforms.
What's Changed
- ble: light the charging pill on a 5/MG, which listens for events nobody sends it by @Zebsi235 in #1826
- ble: handle the pack-attach events on Apple too (#1826) in #1831
- perf(clock): memoise the resolved clock instead of rebuilding it per label (#1829) in #1830
- ble: redact a hex-hidden serial on Apple too (#1833) in #1834
- test centre: log every pushed strap EVENT under the Connection domain by @Zebsi235 in #1825
- android: draw the bottom bar as an overlay so its glass has a backdrop (#1836) in #1837
- android: make the overlay bar actually overlay something (#1836) in #1838
- android: hide the bottom bar while scrolling down (#1839) in #1840
- ios: hide the tab bar on scroll, from the same setting Android uses (#1841) in #1841
- today: carry HRV and Resting HR on the cards, and stop printing -0.0 (#1842) in #1843
- today: lead skin temp with the measured temperature, with a Settings choice (#1844, #1846) in #1845
- health: say when skin temp could not honour the Settings choice (#1847) in #1847
- health: apply the skin-temp preference across the window, not just the newest night in #1850
- health: backfill the skin-temp absolutes the 21-night window never reaches (#1851) in #1852
- health: the absolutes backfill never ran on the installs that need it in #1854
- health: stop predicting which device a night belongs to — read it off the row in #1856
- Revert the skin-temp absolutes backfill (#1852, #1854, #1856) in #1857
- alarm: per-day wake times for the phone alarm (#1858) in #1859
- ble: re-arm a WHOOP 5/MG realtime stream that has lapsed (#1865) in #1866
- ble: stop skipping the handshake once the unbonded probe has retired (#1867) in #1868
- fix(l10n): restore pt-PT Restaurar corrupted by Rest→Descanso by @kvnloo in #1861
- fix(oura): break the resume-cursor ↔ time-anchor deadlock that re-serves the same history forever by @pipiche38 in #1846
- alarm: log what the smart-alarm detector actually saw in #1870
- docs(ios): name the on-device AltStore Error Log shape, not just the desktop one in #1860
- coupled: show the optimal strain band as a tube on Android, matching Apple in #1871
- i18n: add a Russian locale to Android, and backfill Apple's from it in #1872
- i18n(android): park the partial Arabic translation as data, not a locale in #1873
- sleep: drag the filled hypnogram to read the clock time under your finger in #1875
- backup: say WHICH folder backups go to, iCloud Drive included in #1876
- android(backup): clear the busy flag in a finally, as the Apple twin does in #1877
- ble: say what an unbonded strap actually costs, not just history sync in #1878
- today: say why Recovery Vitals is blank on a night...
NOOP 11.1.0
NOOP v11.1.0
A release about NOOP saying what is true, and letting you choose how you read it.
Choose your clock
Times were decided by your device's region, not by you. On Apple that is deliberate — it keeps your
region's date order and clock style — but building a locale from a region identifier throws away the
explicit 24-Hour Time switch in iOS Settings. Someone in a 24-hour country who preferred a 12-hour
clock had no way to get one.
Settings → Appearance now has Clock: System / 12-hour / 24-hour. It defaults to System, so no
existing display changes unless you ask, and System now follows your phone's own switch.
It reaches the times you actually read: the Asleep/Woke row, workout and nap times, caffeine and
hydration entries, backup timestamps, sleep-stage timelines, and the home-screen widgets. Chart axis
ticks deliberately stay 24-hour for now — 12-hour labels are wider and the axis spacing needs looking at
on a real device before that changes.
Sleep without motion
Sleep detection is built on stillness, so a strap that banks no movement produced no nights at all. That
is the everyday reality of a WHOOP 5/MG that will not complete pairing: live heart rate streams fine and
nothing is ever recorded.
NOOP can now find and stage a night from heart rate alone. It is display-only on purpose: an HR-only
night never contributes to your resting heart rate, HRV or SDNN baselines. Those describe your
physiology, and a night detected without the usual evidence has not earned a place in them.
Logs that report instead of asserting
Two diagnostics landed because two different reports could not be answered from a log.
Every disconnect now writes one line saying how long the link held, how many frames and bytes arrived,
whether the realtime stream was armed, and how it ended. Previously a log could not state whether a strap
had transmitted anything — you had to infer it from the fact that every logged line happened to be
outgoing, which measures NOOP's logging rather than the strap.
The clock exchange now quotes the strap's own reply. NOOP wrote "clock synced — strap can persist history
now" the instant it queued the write, before any answer existed, so a log could assert the clock was set
while the connection readout said 1970/71 — two contradictory statements with nothing to tell them apart.
It now says what it sent, and logs the reply frame verbatim. On a 5/MG the readout also said "RTC
reads 1970/71" when no clock had ever been read on that family; it now reports what it actually has,
which is how the strap dated its records.
Everything else
- The 1970/71 warning takes battery into account, so an already-charged strap is not told to charge (#1818).
- The AI coach receives deep, REM and light minutes plus sleep efficiency (#1816).
- Export warns when a backup exceeds the 2 GiB restore ceiling, and an oversize restore can be allowed
through instead of refused (#1807). - Widget heart-rate and HRV values match their icons again, and both platforms' widgets read aloud
properly (#1795, thanks @Sneheth; #1799). - The live-workout timer no longer sits under the pause button (#1814), and the steps calibration card
stops asking for data you have already provided (#1815). - Oura rings that advertise no name label consistently across platforms (#1797, thanks @pipiche38), and
Android's standard heart-rate logging is honest about how much it captured (#1796, thanks @kvnloo).
Still open
WHOOP 5/MG history remains the hard problem. A strap that will not complete the pairing handshake is
never given a clock, and an un-clocked 5/MG does not persist sensor data to flash — so its offloads
complete with nothing in them. The diagnostics above exist to tell that case apart from a strap that was
clocked and still refuses, which is a different fault reported on the same firmware.
What's Changed
- fix(oura): make the nameless-ring label rule one tested twin, not two by @pipiche38 in #1797
- android: twin standardHRHostReceivedLine and be honest about take(200) by @kvnloo in #1796
- widgets: give the home-screen vitals footer the app's own HR/HRV icons in #1798
- widget: let the home-screen HR and battery line say what it is in #1800
- sleep: add an HR-only Stage-0 spine for straps that bank no motion in #1805
- sleep: wire the HR-only spine, on a percentile anchor rather than the median in #1806
- backup: warn at export when a database is past the restore ceiling, and let a user restore it anyway in #1808
- backup: Android half — warn at export, and let a user restore an oversize backup in #1810
- sleep: give the HR-only spine a funnel line, because it shipped silent in #1812
- sleep: stop the HR-only fallback inheriting an exclusion written to keep WHOOP out in #1813
- live workout: stop the bottom timer sitting under the pause button in #1814
- steps: stop the calibration card asking for the half the user already has in #1815
- coach: send the sleep stages it already has in #1817
- rtc: stop telling an already-charged strap to charge it (#1818) in #1819
- diag(ble): log a link epitaph on every disconnect (#1809) in #1820
- Clock format setting: System / 12-hour / 24-hour (#1821) in #1822
- diag(clock): read the strap's answer instead of asserting it (#1823) in #1824
Full Changelog: v11.0.0...v11.1.0
Downloads — NOOP Staging identity, installs beside the official app (separate data):
- Android —
NOOP-android-v11.1.0.apk(com.noop.whoop.staging) - macOS —
NOOP-macos-v11.1.0.zip(unzip, then right-click → Open) - iOS —
NOOP-ios-unsigned-v11.1.0.ipa(AltStore / Sideloadly)
NOOP 11.0.0
NOOP v11.0.0
A major release, and the version number is about one thing above all: the WHOOP 5.0 and MG finally hold
a connection. Everything else — a body clock on the Sleep screen, nights that read straighter, a Journal
that stops counting silence against you — arrived alongside it.
The WHOOP 5.0 / MG connection
A handshake the strap never answers no longer takes the link with it (#1635, #1642, thanks
@Zebsi235). NOOP wrote a CLIENT_HELLO on every connect and the 5/MG never acknowledged it; roughly
4.8 seconds later the link dropped, and the cycle repeated all day. NOOP now recognises the pattern,
stops writing the handshake for that strap, and keeps the link — the "Live HR, not fully paired" state
the app already modelled, reached deliberately instead of never. A field capture confirmed a link held
for over seven hours where it previously lasted five seconds.
An HCI capture contributed by @Zebsi235 settled what the strap is actually doing: it answers the
pairing request with Pairing Not Supported. That is a refusal, not a timing problem, and it ended
weeks of guessing.
Reconnecting costs what it should (#1778, #1779, #1781). Tapping Connect used to throw away
everything NOOP had learned and re-earn it over five more dropped links. It now keeps that knowledge,
gives the handshake exactly one fresh attempt, and settles after a single reconnect. A timer firing is
no longer treated as you asking to try again.
The strap is asked properly, and says so in the log (#1639, #1646, #1648, #1652, #1653, #1656,
#1668). NOOP now observes the OS pairing flow rather than inferring it, reads the Device Information
Service on a strap that never bonds, enumerates what a strap actually offers once per device, and lets
a 5/MG identify itself instead of being guessed at from its serial.
Clearing a stale phone pairing (#1790). When the phone holds a pairing the strap no longer honours,
NOOP can now clear it for you — off by default, in Test Centre, once per streak, and only after the
guide has already asked you to do it by hand.
Sleep
Your body clock (#1722, #1723, #1726, #1729, #1731, #1733). A 24-hour dial on the Sleep screen
showing your circadian phase, with an absolute chronotype bucketed from it. The diagnostics report the
inputs it used, and a rhythm too small to be real is refused rather than drawn.
A partial night is not a whole one (#1717, #1721, #1734). A hypnogram that only partly downloaded
was scored as though complete, which quietly shortened nights after an interrupted sync. Both readers
of that coverage now agree on what "complete" means.
Fragmented nights are stitched back together (#1755, #1782). When data is sparse, NOOP bridges
short gaps between sleep runs — and now allows a longer interruption when your heart rate stayed in the
sleep band throughout, because on a strap with no motion data heart rate is the better witness.
Sleep debt you can act on (#1741). Debt is now expressed as a target for tonight rather than an
abstract ledger.
Journal, workouts and metrics
Yes days are compared against No days (#322). Previously an insight weighed the days you logged
against every day you did not — so a week you never opened the app counted as a week of No.
Pause and resume a live workout (#1690). The controls are on the active-workout card, and the five
clocks that used to ignore a pause now honour it.
Latest Workouts opens a session (#1701, #1703). Tapping a row goes to its detail, and iOS uses the
same 14-day window Android already did.
Skin temperature leads with the absolute (#1663, #1665, #1671, #1708). The nightly absolute is
stored and shown first, deviation beneath it, in your own units throughout.
VO₂max says why a line breaks (#1740). The chart shows the decimal it draws, and explains a gap
rather than leaving one.
Straps, sync and privacy
Multiple straps are kept apart (#1455, #1634, #1730, #1745). Physiology is read across every
registered strap, firmware is attributed to the strap it came from rather than the last to connect,
last-sync belongs to a strap rather than the install, and a 5/MG carries a stable identity derived from
its serial.
Health Connect permissions by category (#1509, thanks @kavemang). Recovery and wellness, Activity,
and Body composition are picked before Android's prompt. Existing installs are detected and keep every
category they already had.
A crash you can report (#1718, #1720, #1724). Android shows a copyable recovery screen after a
crash, redacted everywhere it leaves the device — and an upgrade no longer greets you with a crash from
the build it replaced.
Update checking, on by default (#1674, #1675). Both platforms notice a new release. The privacy
documentation now describes what actually happens.
Experimental self-hosted push (#1631, #1700). One-way push to your own endpoint, off by default,
with the destination errors localized.
Oura
A bonded ring with no advertised name is listed again (#1776, thanks @pipiche38). A bonded ring
often stops advertising a name, and that was exactly the ring the Add-Device screen could not show.
Skin Temp as a Key Metrics tile (#1678), and protocol corrections (#1629, #1687, #1777).
Under the hood
Raw 100 Hz IMU capture and labeled recording sessions for the 5/MG (#1709). A JSON query CLI for the
local read tools (#1752). Analysis reads each row once instead of 2.25 times (#1695, #1696). Swift
analytics tests run on Linux (#1529). Several diagnostics now say which gate refused a metric, rather
than leaving a blank.
Thanks to @bhelm, @pipiche38, @kvnloo, @Zebsi235, @kavemang, @fadkeprasad, @Meriquium and @dwehrmann
for contributions in this release.
What's Changed
- Stop Blood Oxygen promising a reading a WHOOP 4.0 cannot produce in #1618
- Correct the WHOOP 4.0 blood-oxygen copy: the sensor is not the missing part in #1619
- diagnostics: check other device ids before blaming a fresh re-add in #1620
- ble: pick the battery source from evidence, not from a guess in #1623
- analytics: collapse the per-day sleep-skip spam to one line per pass in #1626
- import: report which metric columns an export actually carried in #1628
- diagnostics: make a nil respiratory rate say which gate refused it in #1632
- diagnostics: name every paired device in the strap log (and redact the header) in #1633
- devices: attribute firmware to the strap it came from, not the last to connect in #1634
- ble: report what became of a 5/MG CLIENT_HELLO write in #1638
- ble: stop declaring a 5/MG bond from a completion that is not the CLIENT_HELLO's in #1641
- ble: let the bond give-up see an unanswered handshake in #1640
- ble: observe the OS pairing flow, which NOOP has never watched in #1639
- ble: stop the CLIENT_HELLO from knocking a working 5/MG link off every 5s in #1642
- ui: stop calling a live-HR-only link "Bonded" in #1645
- ble: ask Android to pair, instead of hoping a write provokes it (opt-in experiment) in #1646
- ble: read the Device Information Service on a 5/MG that never bonds in #1647
- ble: ask the device its bond state, instead of trusting our own receiver in #1648
- ble: let the DIS read happen when the pairing experiment is on, and not lie about refusals in #1649
- docs: BLE lessons from #1635, and correct what app-build actually validates in #1650
- ble: capture 5/MG frames outside a sync window, and stop naming an unreachable remedy in #1651
- ble: enumerate what the strap actually offers, once per device in #1652
- ble: read the rest of the Device Information Service in #1653
- iOS: wire Blood Oxygen and Skin Temp on Liquid Today (#1627) in #1654
- docs(#1617): the WHOOP 4.0 is missing a second channel, not a conversion in #1655
- protocol: let a 5/MG say what it is, instead of guessing from its serial (#520) in #1656
- analytics: start Banister's axis where Edwards' starts (#1624) in #1658
- report: render the trends export in the user's own units in #1660
- store: keep the nightly absolute skin temperature in #1663
- charts: make every scrubbable graph read like the screen around it in #1664
- health: lead the skin-temp tile with the absolute, deviation beneath in #1665
- capture: an empty raw capture is no capture, not a successful share in #1666
- diagnostics: stop calling a second strap's night a read failure in #1667
- ble: opt-in switch to send the CLIENT_HELLO despite the suppression latch in #1669
- ble: the bond poll cannot convict Android, and an HCI capture says it is wrong in #1668
- ble: bound the hello override so it cannot loop the link forever in #1670
- ui: show skin temperature in the reader's unit on the alert banner and the fusion row in https://githu...
NOOP 10.6.0
NOOP v10.6.0
A release about choosing how your training is scored, giving hardware back the time it needs to do its
own job, and stopping the phone from doing work it was never going to finish.
Effort, on a scale you pick
Banister TRIMP, wired end to end and selectable (#1562, #1563). Effort has always been a
thresholded Edwards score. Banister TRIMP is now a first-class alternative, complete with its own
log-map denominator (#1561) rather than borrowed constants, and the docs no longer claim NOOP uses a
method it did not. The toggle sits where someone looking for it would look (#1580).
A workout is scored against the day's HRmax (#1565). A session used to be able to score against a
different maximum than the day containing it, which made a hard workout and a hard day disagree. The
strap log now also records what each workout's Effort was scored against, and whether the strap saw the
session at all (#1566) — and when a day detects no workout, it says which gate ate it (#1574).
The phone stops re-scoring all night
A background re-score that can finish (#1557). A re-score that ran out of background time restarted
from the beginning on the next attempt, so on some phones it never completed at all and days simply
refused to compute. It now runs where it can finish, and a pass that is owed is recorded durably so a
process killed mid-way leaves evidence the next one can read.
Diagnostics that name the pass (#1556, #1559). Every re-score now says what triggered it and whether
anything actually changed, and the cache ratio is counted honestly. An active trace no longer disables
the reuse cache it was meant to observe (#1575).
Hardware gets its time back
The Oura ring is handed out of daytime mode (#1526, #1550, thanks @pipiche38). The 15-second
keep-alive held the ring in live-HR daytime mode whenever the app was not on screen — which blocks the
ring's own overnight sleep suite. NOOP now suspends the re-engage and actively disables the mode, on
teardown as well as on suspend. Measured against the ring's own output across eleven nights.
The bond loop can escape while backgrounded (#1544, thanks @justinjor-bit). A pause with no
background escape cost a full night of banked data; it now has one.
Steps calibration asks the right straps (#1579, #1581, #1582, thanks @kavemang). A WHOOP 5 no longer
sees a prompt that cannot apply to it, and a blank Steps tile routes to calibration instead of nothing.
Sleep and Health Connect
The night's heart-rate line, in Classic (#1552, thanks @bartmuskala). The card that was missing its
chart has it. The stage breakdown is ramp-aware and the redundant, mis-ordered legend is gone (#1551).
VO₂max flows to Health Connect (#1553, #1554). NOOP already computed it; it now exports it, and
existing installs are asked once for the permission rather than silently lacking it.
Smaller things that were wrong
- Pause and discard a live workout (#1533, thanks @bhelm), and the SDNN index ported to Android (#1535).
- Back from More returns to More (#1571, thanks @SpurgtBror), not to Today.
- No horizontal bounce on iOS (#1532, thanks @evoveotech), including the four ScrollViews the first sweep missed (#1549).
- Home is localized on both platforms (#1490, thanks @bhelm).
- PPG burst boundaries survive decoding (#1583, thanks @kvnloo).
- A WHOOP 5/MG no longer logs a clock warning it cannot act on (#1609), and no longer derives a clock offset from how long it went unworn.
- The phone smart alarm can skip chosen days (#1612) — a weekend off without turning the alarm off and having to remember to turn it back on.
Thanks
Thanks to @anwrgo6000-oss, @bartmuskala, @bhelm, @Dessalines39394, @evoveotech, @justinjor-bit,
@kavemang, @kvnloo, @mailingjash, @pipiche38, @SpurgtBror and @vishk23 — for the pull requests above and
for the reports behind them. A good bug report with a strap log attached is often the harder half, and
several of the fixes in this release exist because someone took the time to capture one.
What's Changed
- Compare the whole workout key when an edit replaces a row (#1488) in #1489
- Stop "Duplicate as manual" deleting the session it copied (both platforms) in #1500
- Carry the waist into the post-offload pass, so VO₂max stops flipping estimator (#1493) in #1504
- Edit a bridged night across all its fragments, not just the winning one (#1492) in #1506
- Measure how a duplicated second's two copies compare (#1505) in #1510
- Show the steps-calibration prompt to the users it was written for (#1491) in #1512
- Give the Android Steps tile the caption it has been computing all along (#1513) in #1514
- Stop telling a calibrated user they need 0 more days, on both platforms (#1515) in #1516
- Fix broken Star History chart by @Dessalines39394 in #1517
- Fix lifting note title delimiter parity by @bhelm in #1494
- Count rejected Liftosaur history records on Swift by @bhelm in #1497
- Fix Swift SpO2 capability token normalization by @bhelm in #1495
- Fix activity route distance rounding parity by @bhelm in #1496
- Fix trailing-dot activity format detection by @bhelm in #1498
- Fix activity import distance half-tie rounding by @bhelm in #1499
- Fix untimed single-point activity distance by @bhelm in #1508
- Trim capability and key-metric tokens on read (#1518) in #1519
- Hand off to the standing connect on the first drop, not the third (#1413) in #1520
- Give the three hero columns one width, and the label back the space it lost (#1502) in #1507
- perf: drop two redundant hot-path reads added in 10.5.1, plus two staleness/parity fixes found reviewing them in #1521
- fix: stamp a device as seen when it connects, not when it was added (#1527) in #1530
- Localize the Home experience on Android and iOS by @bhelm in #1490
- Port the SDNN index to Android by @bhelm in #1535
- fix: round the imported Apple step total on Apple, matching Android in #1540
- Keep Android intelligence analysis JaCoCo-instrumentable by @bhelm in #1524
- fix: give the bond-loop pause an escape that works while backgrounded in #1544
- 5/MG haptics: carry the repeat count into overallLoop, on both platforms (#926) in #1548
- Fix: stop horizontal bounce/swipe on Liquid Today (iOS) by @evoveotech in #1532
- Finish the #1532 sweep: the four production ScrollViews it missed (iOS) in #1549
- fix(oura): stop the ring's live-HR daytime-HR mode from running while nobody is looking by @pipiche38 in #1526
- Oura: hand the ring out of daytime mode on teardown, not only on suspend in #1550
- Sleep: drop the stage legend from the breakdown card, both platforms in #1551
- Sleep: give Android's Classic view the night's heart-rate line in #1552
- Health Connect: export VO₂ max, which NOOP already computes in #1553
- Health Connect: ask existing installs for the VO₂ max permission, once in #1554
- CI: prune branches that no longer have a reason to exist in #1555
- Re-score diagnostics: attribute every pass, and count the cache ratio honestly in #1556
- iOS: run a backgrounded re-score where it can finish, instead of restarting it in #1557
- Correct what the day-cache signature comment claims, on both platforms in #1558
- CLAUDE.md: the CI table said Android was disabled; it has not been in #1560
- Measure where a re-score pass actually goes, before optimising the reads in #1559
- Give Banister TRIMP its own log-map denominator in #1561
- Wire the Effort TRIMP method end to end on both platforms in #1562
- Make the Banister Effort scale selectable, and stop the docs claiming we use it in #1563
- Correct stale cross-platform twin comments by @bhelm in #1531
- The report-completeness "parity test" could not detect the drift it guarded in #1564
- Score a workout against the same HRmax as the day containing it in #1565
- Log what each workout's Effort was scored against, and whether the strap saw it in #1566
- Give every scoring pass a device registry, and say so on both platforms when there isn't one in #1567
- Say which gate ate the workout when a day detects none in #1574
- Stop an active trace from disabling the re-score reuse cache in #1575
- Stop rendering a one-way permission as a two-way s...
NOOP 10.5.0
NOOP v10.5.0
A release about seeing your training as a shape over time, getting numbers that used to need a tape
measure, and — most of all — your phone doing far less work to tell you the same things.
Training load
CTL, ATL and form (#1423, #1425). NOOP now models chronic and acute training load and the balance
between them, and draws all three on a Trends card. Fitness accumulates slowly, fatigue quickly, and the
gap between them is what tells you whether a hard block is building you up or digging a hole. Computed
on-device from the strain you already have; no new data is collected.
Sleep-versus-wake heart rate (#1424). The contrast between your sleeping and waking heart rate,
surfaced as its own signal rather than buried inside recovery.
A VO₂max you can actually get
Estimated without a waist measurement (#1391, #1406, #1393). The previous estimate needed a waist
circumference many people never enter. If NOOP knows your resting heart rate, it can now use the Uth
heart-rate-ratio method from age and sex alone, and it names which method produced the figure. An
estimate that says how it was reached is worth more than one that doesn't.
Battery
Stop re-scoring days that haven't changed (#1005, #1395, #1396, #1402). NOOP re-analysed a rolling
window every fifteen minutes, including days whose data had not moved since the last pass. A per-day
reuse cache now skips that work, on both WHOOP 4.0 and 5.0/MG. This was the largest background drain on
Android and it was reported, measured and chased down with logs by @bartmuskala.
Low refresh, and Power saving where you can find it (#1430, #1431). A sub-option syncs hourly at any
charge instead of every fifteen minutes — fewer reconnections is the biggest saving on a WHOOP 4.0 — and
Power saving moved into More, with the battery threshold now reaching 35%.
Fewer diagnostics running forever. Shadow instrumentation from the R-R over-count investigation was
doing six sorts of a night's beats per night, on every affected strap, four times an hour. Two
measurements that had finished their job were retired, and what replaced them costs one pass.
Apple Health, and your ring
Automatic Apple Health write-back (#1432, thanks @MikaSchultes). Workouts can flow back into Apple
Health without a manual export.
Hourly step import (#1429). iPhone steps arrive hour by hour, with a 90-day backfill, instead of one
lump per day.
More of the Oura ring decoded, and read honestly (thanks @pipiche38). The ring's own breath rate is
decoded and shown as instrumentation rather than scored (#1384, #1450); its step features are decoded as
Tier-B instrumentation (#1359); the skin-temperature "worn" gate now allows for a ring whose heart-rate
and temperature sensors run on independent clocks, which was blanking the card entirely (#1467); and the
Rhythm view says "no data" outright on hardware that cannot produce the reading, instead of showing an
empty chart (#1360).
Honesty and accuracy
A large part of this release is numbers that were quietly wrong, or quietly guessed:
- A completed sync no longer reports itself as interrupted (#1466). A WHOOP 4.0 routinely ends a
full night's offload on an idle timeout; NOOP called that "sync interrupted" even when the whole night
had landed. - Diagnostic exports report the strap that actually connected (#1451, #1464). Two logs from WHOOP 5
straps were headed "WHOOP 4.0", and on Apple the header had never reported a model at all. - Battery percent refreshes twice as fast while charging, because that is the one time the number
moves while you are watching it. - The strap reconnect survives suspension (#1421), and a 5.0/MG that does serve history no longer
thrashes its link every two minutes (#1414, thanks @justinjor-bit). - Re-scoring no longer silently skips Oura, watch and re-added straps (#1392, thanks @pipiche38).
Cross-platform correctness
@bhelm contributed roughly thirty fixes in this release, almost all of the same kind: places where
the Swift and Kotlin implementations of one calculation disagreed. Half-tie rounding in recovery traces,
FIT coordinates and connection durations; signed zero surviving a clamp; DST-spanning stress windows;
sleep-stage clipping; malformed UTF-8 in the Oura text decoders; JSON control characters in Android
backups; Unicode normalization of custom lab-marker keys. Individually small, collectively the reason
the two platforms produce the same numbers from the same bytes.
@vishk23 extended the golden decoder oracle past frame decode (#869) — and it immediately found four
places where a 32-bit narrowing on Kotlin would have silently discarded all history from 2038, or today
on a strap with a future-dated clock.
@terry-zeng1134 fixed the AltStore manifest's declared permissions (#1388), and @Duzafizzl built
out the Breathe protocol catalog with holds, session lengths and translations (#1390).
Thanks
To @bhelm, @pipiche38, @vishk23, @MikaSchultes, @Duzafizzl and @terry-zeng1134
for the code; and to @bartmuskala, @justinjor-bit and @pipiche38 for reports carrying logs,
screenshots and measurements — several of which found bugs no test was going to catch.
And a belated thank-you to @gdorgian (#1366), @riebschlaegerstar-sys (#1361) and
@pilleuspulcher-blip (#159), whose reports were fixed in v10.1.0 but went uncredited in that
release's notes.
What's Changed
- feat(today): add VO₂max as an opt-in dashboard card (#1391) in #1393
- fix(analyze): re-score gate must fingerprint HR across ALL devices, not "my-whoop" (#1392) in #1394
- perf(analyzeRecent): per-day in-memory reuse cache — fix #1005 re-score battery drain in #1395
- perf(analyzeRecent): extend the per-day reuse cache to WHOOP 5.0 / MG (#1005) in #1396
- fix(steps): honest "connect a phone step source" prompt when a 4.0 has no step reference in #1397
- fix(storage): count stepSample + ppgHrSample in the decoded-rows footprint in #1398
- feat(breathe): ANS/Presence protocol catalog, holds, session length, i18n by @Duzafizzl in #1390
- fix(altstore): declare healthkit.access, location, and motion permissions by @terry-zeng1134 in #1388
- diag(hrv): size the cross-second R-R over-count with a windowed shadow candidate (#1331/#1118) in #1399
- diag: capture the 4.0 GET_BATTERY_LEVEL frame in the strap log (#900) in #1400
- Baseline-relative daytime stress (revives #463) — opt-in + full parity in #1401
- perf(analyzeRecent): stop the #1005 cache churning on baseline drift during a backfill in #1402
- diag: add the computed '-noop' spine to the Recent-Nd data line in #1403
- fix(android): wire the VO₂max card's trend tap-through (#1391) in #1404
- fix(android): route vo2max_est to its series trend — completes #1404 (#1391) in #1407
- feat(fitness): offer a VO₂max without a waist via the Uth HR-ratio fallback (#1391) in #1406
- #1391: settings VO₂max sub-text — note it builds from ~4 nights of HR in #1408
- #1405: de-collide the Charge synthesis word from the readiness verdict in #1409
- fix(ble): read the reboot-ack result at the family's offset (5/MG @12) in #1411
- fix(live-session): guard the upsert so a late start-write can't erase the final row in #1412
- fix(healthkit): commit the observer anchor only after ingestion, not before in #1415
- fix(watch): tick the workout clock off the live builder, not sample callbacks in #1416
- fix(ble): widen the 5/MG liveness fuse to the whole family (#1414) in #1418
- feat(widget): keep showing the last heart rate through a quiet patch in #1419
- fix(hrv): hold the spot capture to a real 60-second window by @bhelm in #1417
- DRAFT: generation-side 0x49-onset keying for Oura sleep (#1284 residual 3) in #1383
- fix(ble): standing-connect reconnect regime for WHOOP — survive suspension (#1413) in #1421
- feat(backup): record the app build in exports + on version change (#1410 tiers 1-2) in #1422
- feat(analytics): CTL/ATL/TSB training-load model (Swift + Kotlin) in #1423
- Make the v20 optical byte map executable: name its eleven fields, CRC-gate the decoder by @vishk23 in #895
- feat(analytics): sleep-vs-wake HR contrast (Swift + Kotlin) in #1424
- feat(trends): CTL/ATL/TSB training-load card (iOS/macOS + Android) in #1425
- refactor(sleep): split SleepScreen UI into sibling files (re-cut of #734) in #1426
- fix(ble): pair WHOOP4 clock correlation with its capture wall, not now in #1427
- fix(settings): waist row label vanished behind ~300dp of dead space (+ copy parity) in #1428
- feat(apple): hourly step import — per-hour iPhone steps with 90-day backfill (re-cut of #369) in #1429
- feat(power): "Low refresh" hourly sync...