NOOP 10.0.0
NOOP v10.0.0
A release about making NOOP feel like yours, without changing what it is. You can now colour the app
with a chosen accent, save a whole look as a preset, and put your own photo behind every screen. The
workout picker learned dozens of new sports, a run can carry its distance and a GPS route you can
export, and the Workouts screen grew a heatmap of your recent effort. Alongside the personalisation,
a batch of sleep and calorie work makes several numbers more honest. Everything still runs on your own
device, offline, with no account.
Scores that change
Read this before wondering why a number moved. A few changes alter values you have already seen.
- Active calories use a fitness-adjusted model when your resting heart rate is known (#1268). NOOP
now estimates burn with the Keytel VO₂max-based equation instead of the age/weight-only one, which
moves already-stored calorie figures. History computed before this stays as it was; new days use the
better model. - Oura sleep reads less phantom wake (#1246, #1248, #1293, #1296). Erased and partly-written
hypnogram pages were being decoded as hours of "awake"; they are now correctly treated as a gap, so
affected Oura nights show higher efficiency and less wake. A night scored before the fix keeps its
old value until re-imported. - An Oura night is scored from the ring's own staging (#1183). Feeding Oura's hypnogram straight
into the scorer, rather than re-deriving it, changes the stage split on Oura nights. - Sleep-stage percentages sum to exactly 100 (#1282), and the nap card's "Main sleep" now describes
the whole bridged night to match the hero and iPhone (#1266, #1267) — small display shifts, not new
measurements. - Android sleep Consistency matches the iPhone definition (#1233). Both platforms now compute it
from bedtime-onset spread, so the two stop disagreeing. - Stress is steadier (#1114, #1116). The daytime timeline masks exertion instead of scoring
movement as stress, and an HRV dip now has to sustain before it nudges the score.
New
Make it yours. A chrome accent colour you choose — Mint, WHOOP Blue, or a custom colour from a full
HSV picker (#1171, #1177) — with Android brought to brand-mint parity with iPhone (#1170). Named theme
presets coordinate the accent, charts, backdrop and cards as one look (#1172). And a custom background
image sits behind every tab, on all three platforms (#1234, #1235, #1236), with the Liquid hero card
made theme-aware in light mode along the way (#1160, #1162).
Workouts. Around forty new sports across two passes (#1273, #1274), plus a Start-and-Add action row
and sport-icon picker on Android (#1079). A manual workout takes a Distance field (#1203); a GPS workout
records live distance and pace (#1202) and writes manual and edited workouts back to Health Connect
(#1204); and a finished route exports to GPX or FIT (#1238, #1239). The Workouts screen gains a 13-week
active-calorie heatmap (#1240), and Compare charts gain value scrubbing (#529).
Sleep. Choose the hypnogram's look — Classic, Fill, Garmin Fill or Ribbon — each with a colour-coded
stage legend, on every platform (#1129, #1283, #1290, #1291). Today can now host your Trends and Sleep
cards — Sleep marks, Asleep duration, and more — so your most-watched detail lives on the home screen
(#1230, #1231, #1232), and a "sleep may be incomplete" caveat appears on sparse-motion nights (#1096).
Metrics and language. A language setting independent of the phone (#1181, #1004). Skin temperature
can read as an absolute °C or a Δ from your baseline (#1224). WHOOP 5.0 SpO₂ is available behind an
experimental toggle (#1185), and the WHOOP 4.0 battery-pack voltage path is decoded (#1279). Per-event
opt-in haptics for in-session strap cues, and an opt-in wrist buzz for the phone's own timer and alarm
(#1115, #1158, #1159).
Fixed
- A connected strap with nothing arriving no longer flickers the daily history to empty (#1196), and
a post-offload re-score stops erasing days it should have kept. - A WHOOP 5.0 mislabelled as a 4.0 is corrected from the strap's own DIS attestation (#1175), and a
5.0/MG can turn Broadcast-HR off again with an honest log line (#1061). - Two straps no longer lose a row to a duplicated day (#1190), and the charging pill flips on the
actual charge event rather than only on a battery-level change (#1189). - The strap log keeps its last three generations (#1259, #1263), so a restart stops erasing the very
cause it was recording. - A non-WHOOP device stops inheriting WHOOP-5 empty-state copy (#1285), respiratory rate is correct
for BLE-only WHOOP users (#1178), and the Android SpO₂ and Rest-ring captions are localised and sized
(#1184, #1186). - Oura reconnects survive CoreBluetooth suspension (#1286), 0x5D battery buckets are read oldest-first
(#1182), and banked-sleep duplicates heal across every device rather than one id (#1248).
Under the hood
Analytics and the live-HR hot path shed avoidable churn with no change to any value (#1214). A pure
primary-session resting-HR definition is recorded as a shadow metric with its coverage inputs, ahead of
any wiring (#1169, #1174, #1188, #1211). Forced re-scores are attributed as new-data yes/no so a battery
investigation can tell a real refresh from a spin (#1192), and a raft of Oura protocol diagnostics make
the decode legible from the strap log alone (#1284 and the #1043–#1131 series).
Thanks
Thanks to @pipiche38, @vishk23, @whisp0, @TAKEOFF69, @Quaii, @liaolo, @evoveotech, @adrianlaureda,
@digitalerdude, @ben-tsk, @SpurgtBror, @Creanathan and @tigercraft4 — for the deep Oura protocol and
sleep-decode work, the stress-model refinements, the Apple BLE timing fixes, the Home Screen quick
actions, the Android accent parity, and the field reports and strap logs behind many of the fixes above.
What's Changed
- fix(android): trust Windows aapt2 checksum by @TAKEOFF69 in #1062
- fix(android): close historical backfill drain handoff race by @TAKEOFF69 in #1063
- fix(oura): label the R-R optical channel so scoring stops reading every beat twice by @pipiche38 in #1076
- fix(oura): give each SpO2 sample its own second so 12 of 13 stop coll… by @pipiche38 in #1075
- Android trends: neutralize card surfaces (EXP-004 parity) in #1080
- Android live workout: glanceable vertical-stack parity in #1077
- Android workouts: Start + Add action row (EXP-018 parity) in #1079
- fix(android): order Room schema snapshot after test KSP by @TAKEOFF69 in #1064
- Fix sleep edits not preserving the wake calendar date by @Creanathan in #1059
- fix(oura): hand the store a record's beats at once so beat order stops being 0 by @pipiche38 in #1082
- fix(ios): AI Coach robustness parity with Android (#1074) in #1087
- fix(android): AI Coach fixes for #1074 (crash, Disconnect UI, truncation, real errors, model scan) in #1083
- fix: sum activity-file steps across a day instead of overwriting (#1058) in #1088
- docs: drop dead noopapp Homebrew tap, point macOS users to Releases (#1069) in #1090
- fix(hrv): stop reporting SDNN for a capture already known to be over-counted by @pipiche38 in #1085
- fix(oura): give the bare 0x44 IBI tag its own channel code, not 0x60's by @pipiche38 in #1084
- Cache foreground-service daily analytics by @whisp0 in #1051
- perf(ios): memoize the Live Activity widget-anchor across live-HR ticks in #1091
- perf(android): memoize the HR-zone set across live-HR ticks in coachZone in #1092
- diag(sleep): one-line detection summary to make fragmentation legible in #1094
- feat(sleep): "sleep may be incomplete" caveat for sparse-motion nights (#345) in #1096
- i18n(sleep): localize the iOS "May be incomplete" badge in #1098
- fix+debug(ble): 5/MG bond watchdog — early re-pair guide for a silent CLIENT_HELLO + bond-state logs (#1095) in #1102
- Raw history archive: decide on the layout version, and stop empty records evicting real ones by @vishk23 in #1101
- Fix empty macOS "Choose a backup" restore sheet (#1093) in #1105
- Resolve a non-WHOOP device's family from its brand, not a fall-through (#1086) in #1106
- fix(oura): gate banked samples at "now", quarantine future-stamped R-R beats (#1073) in #1107
- fix(hrv): refuse SDNN when the R-R stream is banked, not only when it… by @pipiche38 in #1108
- Docs/oura protocol met motion and 0x61 by @pipiche38 in #1109
- iOS: report BGTask export success accurately and add expiration handler by @tigercraft4 in #955
- Break the BGTask completion-guard retain cycle (follow-up to #955) in #1111
- Refactor Settings shared helpers by @tigercraft4 in #732
- fix(oura): decode 0x5D hrv_event as (u8 hr_bpm, u8 rmssd_ms) 5-min pairs by @pipiche38 in #1110
- Sleep tab: customise & reorder cards (like Today) in #1112
- feat(stress): motion-gate the daytime timeline — mask exertion, don't score it as stress by @vishk23 in #1114
- feat(stress): require an HRV dip to SUSTAIN before it nudges by @vishk23 in #1116
- docs(oura): document the confirmed macOS pairing limitation by @pipiche38 in #1113
- hrv: sample the densest second on an over-count night (#1008) in #1117
- onboarding: fix LocalLifecycleOwner-not-present crash on Activity attach in #1119
- android: advance the #836 watermark after the post-backfill re-score in #1120
- test centre: adb-like detailed capture — rolling on-device strap-log file in #1121
- sleep: opt-in FILLED stepped hypnogram (Android), toggle back to classic in #1122
- ble: count empty-offload stalls toward the backfill backoff (battery) in #1123
- analyze: halve the idle backstop re-score cadence to 30 min (#836 battery, both platforms) in #1124
- ble: count empty-offload stalls toward the backfill backoff on iOS/macOS (#1123 parity) in #1125
- sleep: smooth + seamless the filled hypnogram to match the reference in #1126
- sleep: add a RIBBON style for the stepped hypnogram in #1129
- sleep: responsive round-hour time axis for the stepped hypnogram in #1130
- sleep: denser hypnogram time axis on narrow screens + hour-only, locale-aware marks in #1133
- sleep: widen hypnogram axis edge guard for 12h labels in #1134
- settings: NOOP Limitations capability grid (4.0 vs 5.0/MG) in #1135
- settings→more: relocate NOOP Limitations into the More tab / sidebar (all platforms) in #1137
- docs(oura): record the accelerometer scale candidate and that the 0x47 axes are unnamed by @pipiche38 in #1136
- docs(readme): document Oura ring support — what works, what's coming, what can't by @pipiche38 in #1132
- fix(oura): gate RSA respiration to no-data when the R-R stream is banked by @pipiche38 in #1127
- fix(ios): protect reject-history archive for locked background sync in #1138
- Add leeway to Apple BLE maintenance timers by @whisp0 in #1052
- ble: keep the maintenance tick exact — scope timer leeway to the offload timer only in #1139
- fix(ios): don't resubmit the scheduled-export BGTask once disabled in #1140
- settings(android): in-app Reduce Motion (quiet motion) control — parity with Apple in #1141
- Batch Android stream writes in one Room transaction by @whisp0 in #1050
- fix(android): reject manual workouts ending in the future by @TAKEOFF69 in #1067
- workouts(apple): reject a manual row whose end lands in the future (parity with #1067) in #1142
- fix(oura): drop the 0x5D
00 00tail padding instead of storing it a… by @pipiche38 in #1131 - iOS: avoid full widget rebuilds for live-only updates by @whisp0 in #1055
- widget(ios): read the App Group once on the live fast path (follow-up to #1055) in #1143
- ble: stop the auto-continue empty-offload storm — guard 2a on real rows in #1145
- capture: summarise frameTiming into one rolling line instead of per-transition in #1152
- fix(android): settle MTU before service discovery by @TAKEOFF69 in #1066
- ble: instrument the actual MTU settle time (follow-up to #1066) in #1153
- fix(diagnostics): analytics funnel finds computed-only sleep nights (#1150) in #1155
- fix(android): hold-to-drag reorder for Sleep cards, matching Today (#sleep-layout) in #1156
- fix(devices): keep strap firmware on the card via a last-known fallback in #1157
- fix(ble): throttle 5/MG battery poll + stretch empty-history backfill by @evoveotech in #1154
- feat(haptics): per-event opt-in toggles for in-session strap cues (#1115) in #1158
- feat(android): opt-in wrist buzz for the phone's native timer/alarm (#1115) in #1159
- fix(theme): make the Liquid hero card theme-aware in light mode (#1160, #1161) in #1162
- Add tolerance to the Apple live-session timer by @whisp0 in #1053
- Make the idle→LOW_POWER throttle reachable so #477 can be validated in #1035
- docs(ble): correct the idle-throttle field doc that #1035 made stale in #1165
- ECG raw-data gate: an opt-in enable_raw_data_w_ecg write with a mandatory read-back (#891, #103) in #1166
- feat(android): recolor chrome accent to brand mint (parity with iOS #1068) in #1170
- Refine the iOS visual system and key screen layouts by @liaolo in #1068
- feat: user-selectable chrome accent colour (Mint / WHOOP Blue / Custom) in #1171
- feat(theme): named theme presets (Mint / Ocean / Classic / Midnight / Frosted) in #1172
- analytics: primary-session mean resting-HR definition, pure + unwired (#1169) in #1174
- fix(ble): correct a WHOOP 5.0 mislabelled 4.0 from the strap's DIS attestation in #1175
- feat(android): HSV colour picker for the custom accent (replaces RGB sliders) in #1177
- fix(android): close the Appearance gap above the Sleep-chart control (#1129) in #1176
- fix(ios): respiratory rate parity for BLE-only WHOOP users in #1178
- i18n: extract 3 StrandDesign a11y labels via existing translations (#920) in #1179
- Add language settings, normalized language in app (#1004) in #1181
- fix(android): center + shrink the Rest ring's calibrating subtext (#1168) in #1184
- fix(oura): score an Oura night by feeding the ring's own hypnogram into the scorer (#804 Fix A) by @pipiche38 in #1183
- fix(oura): 0x5D buckets are stamped in reverse — the first pair is the OLDEST (#1167) by @pipiche38 in #1182
- feat(spo2): surface WHOOP 5.0 spo2_candidate_82 behind experimental toggle (#103) in #1185
- fix(spo2): localize the Android @82 estimate captions + align wording (#103 follow-up) in #1186
- chore(android): clear all Kotlin compiler warnings (tests + main) in #1187
- feat(analytics): record primary-session mean RHR as a shadow metric (#1169) in #1188
- Liquid Today: remount the illness banner, the auto-workout prompt, and the hydration value by @vishk23 in #980
- fix(ble): flip the charging pill on CHARGING_ON/OFF events, not just BATTERY_LEVEL in #1189
- fix(data): coalesce a duplicate day per column so two straps don't lose a row in #1190
- fix(diagnostics): read Last sleep/recov from the merged spine, not the import spine in #1191
- feat(diagnostics): attribute forced re-scores as newData yes/no (battery) in #1192
- fix(analytics): stop the post-offload re-score flickering daily history to empty (#1196) in #1197
- fix(ble): gate empty-offload auto-continue on the sensor-record verdict, not a re-read counter (#1146) in #1198
- Add "Forget device" hard-delete for archived rows (iOS + Android) — #1193 cleanup in #1194
- harden(backup): warn on stale auto-backup + verify the written .noopbak in #1199
- fix(ble): let Broadcast-HR be turned OFF on 5/MG + make its log honest (#1061) in #1200
- Read back the Broadcast-HR flag write and report what the strap stores (#1061) in #1201
- Show live distance & pace on the active workout screen (#1195) in #1202
- Add a Distance field to the manual add/edit workout sheet (#1195) in #1203
- Write manually added/edited workouts back to Health Connect (#1195) in #1204
- Nudge the Today header controls up 2pt (34→36), both platforms in #1207
- Match the Today sync chip height to the header controls (#1207 follow-up) in #1208
- Menstrual cycle tracker (#718 by @Jordan2139, rebased + i18n fixes) in #1209
- Sleep: recover strap-disputed false wakes via the band sleep_state veto (Swift + Kotlin) by @vishk23 in #738
- Store primary-session RHR coverage inputs beside the shadow mean (#1169) in #1211
- Move PuffinFrameRecorder encode+write off the main actor (#652, reimplements #958 by @tigercraft4) in #1212
- Reduce analytics + live-HR hot-path churn (no behavior change) in #1214
- Give the Oura central an iOS state-restoration identifier (#1213) in #1215
- Improve layout, spacing, and captions across core screens by @Quaii in #967
- Android: label the Trends change indicator "Trend" (parity with #967) in #1219
- Lab Book: read WHOOP's own biomarker CSV export directly (#1220) in #1222
- Drop calibrated SpO₂ from live WHOOP capabilities (#548) — supersedes #1216 in #1223
- fix(ios): refresh Today after Apple Health sync by @adrianlaureda in #1218
- Label skin temp: absolute °C vs Δ°C baseline deviation (#622) — supersedes #1221 in #1224
- Drive live strap battery % from the pushed BATTERY_LEVEL event (WHOOP 4.0 blanks to "—") in #1226
- Fix skin-temp app-target tests after the #1224 Δ / vs-baseline relabel in #1228
- Today: replace the 2-day Key Metrics trend window with 1 month in #1227
- Android: show the per-metric glyph on each Key-Metrics tile (iOS parity) in #1229
- Today: host Trends/Sleep cards in Today — P0 (infra + Sleep marks) in #1230
- Today: host the Asleep-duration Sleep card (P1 — first Sleep detail card) in #1231
- Sleep: make Hours-vs-Needed & Consistency arrangeable cards (both platforms) in #1232
- Sleep: align Android Consistency analytic to iOS (bedtime-onset spread) in #1233
- Custom background: pref + fill-mode foundation (parity-pinned) in #1234
- Custom background: live feature on Android (renderer, backdrop, Settings) in #1235
- Custom background: iOS + macOS parity (store, backdrop, Settings) in #1236
- Custom background: auto-enable on first pick in #1237
- Workout route export: GPX + FIT builders (Swift + Kotlin twins) in #1238
- Workout route export: Export-route UI + share (iOS/macOS + Android) in #1239
- Workouts: 13-week active-calorie heatmap (Swift + Kotlin) in #1240
- Trends: share the weekly recap as an image (Swift + Kotlin) in #1242
- Custom background: fix the empty gap under the Scaling control in #1243
- Custom background: recent-image presets (last 3, per-image scaling) in #1245
- diag(sleep): log why a night with HR staged to no sleep (#1244) in #1247
- fix(oura): heal banked-sleep duplicates across every device, not just computedId (#1248) in #1249
- fix(oura): decode unwritten 0xFF hypnogram pages as a gap, not awake (#1246) in #1251
- feat(today): group the Customise 'Available' cards by origin (Sleep/Trends) in #1252
- i18n(android): localize the sleep/customise card-catalog titles in #1253
- refactor(sleep): extract a shared SleepModel.build() out of SleepView in #1254
- feat(today): host the Stages-vs-typical sleep card in #1255
- feat(today): host the Night-detail sleep card in #1257
- feat(today): host the Sleep-debt-ledger sleep card in #1258
- feat(today): host a read-only Stages (hypnogram) sleep card in #1260
- feat(today): host the Hours-vs-Needed sleep card in #1261
- feat(today)+fix(sleep): align Consistency score + host the Consistency card in #1262
- fix(diagnostics): keep the last 3 strap-log generations, so a restart… by @pipiche38 in #1259
- fix(diagnostics): roll log generations on manual export too (#1263) in #1264
- feat(android): strap-log durability across restart (parity, #1263 item 2) in #1265
- Sleep: nap-card "Main sleep" total = whole bridged night (Android parity with iOS) in #1266
- Sleep: nap-card "Main sleep" uses decoded group stages, byte-exact with iOS (#1266 follow-up) in #1267
- Calories: use the Keytel fitness-adjusted (VO2max) model when a resting HR is known in #1268
- Heatmap: scale the activity ramp to the 90th-percentile day (floored), not the max in #1269
- Heatmap: quarter total + streak header and month/weekday axis labels in #1270
- Heatmap: outline today's cell + slightly rounder boxes (closer to #222) in #1271
- Heatmap: kcal unit beside the quarter total in #1272
- Workouts: add 30 more sports to the picker (+ Ice Hockey parity) in #1273
- Workouts: add 9 long-tail sports (Rucking, Archery, Fishing, Hunting, Curling, Netball, Gaelic football, Spikeball, Sand volleyball) in #1274
- Workouts: bump the Recent-sports cap 3 → 5 in #1275
- Add Android Compare chart value scrubbing by @kavemang in #529
- Compare tooltip i18n (#529 follow-up): baseline the no-value dash + localize the date in #1276
- WHOOP battery pack: decode GET_BATTERY_PACK_INFO (151), both platforms in #1277
- BatteryPackInfo: byte-exact serial parity + shared edge-vector tests (#1277 follow-up) in #1278
- WHOOP battery pack: add the WHOOP 4.0 voltage path (command 98) in #1279
- WHOOP battery pack: clarify 4.0
presentis not a pack-attached signal in #1280 - Sleep: apportion stage %s so the four shares sum to exactly 100 in #1282
- iOS/macOS: port the Sleep chart style setting (Classic / Filled / Ribbon) in #1283
- diag(sleep): flag a computed day with a sleep total but no matched session (#674/#1244) in #1287
- fix(oura): keep a reconnect outstanding with CoreBluetooth across suspension (#1213 follow-up) by @pipiche38 in #1286
- chore(oura): drop the now-dead nextReconnectDelay() (#1286 follow-up) in #1288
- Sleep: brand-matched colour ramps for the stepped hypnogram styles in #1290
- fix(ui): stop a non-WHOOP device inheriting WHOOP-5 empty-state copy (#623/#1086) by @pipiche38 in #1285
- fix(sleep): give the brand hypnogram ramps a light-mode variant (#1290 follow-up) by @pipiche38 in #1292
- Sleep: split Fill/Garmin Fill styles + colour-coded legend in #1291
- oura(sleep): trim trailing 0xFF pad in a partly-written page + log the #899 sweep (#1284) in #1293
- sleep(#1210): shadow-instrument the dormant band wake-veto (output-neutral) in #1295
- iOS: Home Screen quick actions by @Quaii in #1294
- oura(sleep): clamp the trailing-0xFF floor to >= 2 so a lone byte can't be eaten (#1284) in #1296
- oura(sleep): log the duplicate-session generation moment (#1284 residual 3) in #1297
New Contributors
- @TAKEOFF69 made their first contribution in #1062
- @Creanathan made their first contribution in #1059
- @whisp0 made their first contribution in #1051
- @evoveotech made their first contribution in #1154
- @liaolo made their first contribution in #1068
- @adrianlaureda made their first contribution in #1218
Full Changelog: v9.3.1...v10.0.0
Downloads — NOOP Staging identity, installs beside the official app (separate data):
- Android —
NOOP-android-v10.0.0.apk(com.noop.whoop.staging) - macOS —
NOOP-macos-v10.0.0.zip(unzip, then right-click → Open) - iOS —
NOOP-ios-unsigned-v10.0.0.ipa(AltStore / Sideloadly)