Repository navigation
iOS/macOS: clarify overnight-only HRV setting copy (#92) - #120
Merged
Merged
Conversation
Mirror the Android copy clarification (merged #95) onto the shared iOS/macOS SettingsView so both platforms explain the 'Overnight only' continuous-HRV setting the same way: continuous background HRV capture (incl. daytime naps) is paused outside the quiet-hours window, and for an on-demand daytime reading the user should use the 'Take an HRV reading' button on the Live screen. Copy-only, verbatim match to the Android string now on main. No behaviour change (the overnight-window logic + the manual-snapshot realtime bypass are unchanged); verified both referenced facts against the code — the Live-screen button exists with that exact label, and the snapshot arms the R-R stream via screenWantsRealtime regardless of the overnight gate. Addresses the iOS half of #92.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
iOS/macOS: clarify overnight-only HRV setting copy (ryanbr#92)
ryanbr
added a commit
that referenced
this pull request
Aug 27, 2026
…nothing (#1686) @UncreativeX's capture shows four consecutive completed offloads that banked no sensor rows, and a strap whose newest stored record is 24 days old. NOOP knew that second fact - GET_DATA_RANGE gave it, and the client already logged the raw offset as "2128521s" - and never said it in a form anyone would read. The user was told only "the strap banked no sensor history", which is the SAME line a perfectly healthy caught-up strap produces. That ambiguity is why #1541 has stayed open and unactionable since May. This is the counterpart futureRtcLine never had. That one handles a strap whose clock runs AHEAD: it names the day count and gives the remedy. Nothing handled a strap whose newest record sits far in the PAST. The wording states the FACT and lets the condition carry the interpretation: "the strap's newest stored record is about 24 day(s) old. If you have worn it since then, it has stopped saving history to its flash." A strap left in a drawer shows the same number innocently, so asserting a corrupt RTC would claim more than the data supports - which is exactly how this area has misled people before. It also says the part the standing advice omits. NOOP re-sends SET_CLOCK to a WHOOP 4.0 on EVERY connect, in both firmware payload forms (#120), so "charge to 100% and reconnect" has effectively been retried every session for 24 days. Telling this user to do it again was never going to help. The line gives the real escalation - charge, Restart strap, forget and re-pair - and the one test that settles whether NOOP is involved at all: if the official WHOOP app is also missing those days, the strap is the cause. CHECKED FROM THE CODE BEFORE WRITING ANY OF IT. The "banked nothing" verdict requires decodedChunks == 0, so this is NOT records decoded and then dropped by the #547 gate - there was nothing to decode, and the existing attribution is honest. The backoff is not trapping them either: MANUAL is never floored, and CONNECT/FOREGROUND use the flat 90s event floor with no multiplier. Three review passes, three finds: - THE BANNER, not just the log. The first version put the age only in the strap log, which a user sees by exporting a capture. The message they actually read still said "Fully charge it to 100%, then reconnect, and it should start banking again" - omitting the age, and PROMISING a recovery that has not happened in three weeks. A fix aimed at making this actionable was reaching the one audience that already had the log. staleRecordBanner is the same honesty in banner length, and a test asserts the promise cannot come back. - the field was read twice, once for the log and once for the banner, so a mutation between them could have had the two disagree about whether the strap is stale. Read once now, in the enclosing scope both consume. - PARITY: the twinned functions matched, the INPUT did not. strapNewestTs is gated on feedsSync in Swift (#695 - WHOOP4 today) and set for any family that answers GET_DATA_RANGE on Android, so a 5/MG would have shown the dated wording on one platform only. Both reads are now gated on WHOOP4 explicitly, including where it is redundant today, so a later widening of feedsSync cannot silently invert which side fires. The pre-existing population difference is left alone: changing it would alter 5/MG sync behaviour, which is #695's business. Second, smaller fix in the same area. "Backfill: skipped (FOREGROUND) - policy floor not met (empty streak 4)" appended the streak for EVERY trigger, but the streak - and the future-dated clock note beside it - gate PERIODIC and STRAP only; a FOREGROUND skip is just "58s since the last sync against a 90s floor". The line named a cause it could not attribute, and its practical cost was real: it misled me while reading this very capture. Android only, because Swift has no such line. Verification: the rules are pure and twinned, and BOTH staleRecordLine and staleRecordBanner are proven byte-identical across platforms by extracting and comparing. 12 tests (6 per platform), including the reporter's real numbers - newest 1785692420 against wall 1787820941 - producing "about 24 day(s) old", the two-day boundary, a night off-wrist staying silent, a future-dated record staying futureRtcLine's job, and that neither string says "corrupt" or promises banking will resume. Also compiled standalone with swiftc against those same capture values. Android 4576 tests green, compile clean, doc_comment_lint and i18n clean. app-build green on Build Strand, Test Strand and Build NOOPiOS. Every in-repo surface carrying the old advice was swept; what remains is release notes and changelog entries, which are historical records and stay as written. The Troubleshooting WIKI still carries it and is not in this repo - worth updating, since the issue template points reporters at exactly that page. What this does NOT do: make the strap bank again. If it has genuinely stopped writing to flash that is strap-side. It tells the user that, with evidence, instead of leaving them to re-charge for another three weeks.
This was referenced Sep 2, 2026
ryanbr
added a commit
that referenced
this pull request
Sep 2, 2026
Second re-review catch, and the same mistake the original bug was made of. The charged branch asserted that NOOP re-sends the clock on every connect. That holds on WHOOP4, where runConnectHandshake calls SET_CLOCK unconditionally in both payload forms (#120). It is false on a 5/MG: that clock write is gated behind didBond, and a strap that answers SMP with Pairing Not Supported (#1635) never sets didBond, so it is never clocked at all - which is exactly the strap most likely to be sitting at 1970/71 with this warning on screen. So the sentence would have confidently told the affected 5/MG users something untrue about their own device, in a warning whose whole purpose is to stop giving advice that does not apply. The charged branch now says only what holds for every strap - that charging again will not change it - and asks for the log. The mechanism belongs in the issue, where it can be model-specific; a UI string shown to every model cannot be. Tests pin the absence of the claim on both platforms so it is not reintroduced. Kotlin: 21/21, forced re-run rather than a cached task result.
laurence-cw
added a commit
to laurence-cw/loop
that referenced
this pull request
Oct 5, 2026
…et unarmed WHOOP 4 Two fixes from one field WHOOP 4.0 log (both Apple and Android, with twin tests): 1. Clock after a reboot. The clock was set only in the connect handshake. When the strap rebooted on a flat battery (BOOT, RTC_LOST) while the link stayed up, it banked no usable history until a later connect set it: ~66 h lost after 26 Sep, ~9.5 h after 20 Sep. A live BOOT / RTC_LOST event now re-sets the clock at once (and re-asks GET_CLOCK on Apple so the correlation follows), over a live bonded link, at most once per 30 s. The same SET_CLOCK / GET_CLOCK the handshake sends; nothing new reaches the strap. Android ignores replayed offload events. 2. Liveness fuse. The 120 s 'no data' bounce assumes a streaming strap. An UNARMED 4.0 (no Live screen, Continuous HRV capture off, the default) is quiet between ~8-min battery events, so 100 of the install's 112 disconnects in a day were this bounce, each re-running the offload (327 that day). 120 s now applies only while realtime is armed; otherwise 600 s, as the 5/MG family already has (ryanbr#1414). Verification: StrandTests 2279 pass (StrapClockAndLivenessTests 7 new); NOOPiOS builds; doc_comment_lint OK. Kotlin + tests written, NOT compiled (no JDK). Not yet verified on the strap: expect far fewer 'No data for >120s' lines and an immediate 'setting its clock now' line after the next flat-battery reboot. Refs ryanbr#1414, ryanbr#120 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Oct 31, 2026
SET_CLOCK's 8-byte body is [seconds u32 LE][subseconds u32 LE] in 1/32768 s, the unit of the EVENT header u16 at frame 16 and of the alarm body. NOOP sent the fraction as 0, flooring the phone time to the whole second. A 2026-10-31 MG capture (fw 50.41.1.0) sat 1.3 to 2.1 s behind after each set while the strap RTC drifted under 3 ppm over 5.7 h, so the offset came from the set itself. OpenStrap and meta1971/noop-mg-ecg send the same fraction on MG straps. New pure builder SetClockPayload.withSubseconds(epochMs:) in WhoopProtocol plus its Kotlin twin, both pinned by matching tests. The 5/MG connect path and sendSetClockBothForms use it on iOS/macOS and Android; WHOOP 4 keeps the zero-fraction 8-byte and 9-byte forms validated on fw 41.17.x (ryanbr#120). Not yet verified on hardware: the next strap log after a connect should show the GET_CLOCK reply within a fraction of a second of the phone. Analysis migration required: no
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
iOS/macOS half of #92, mirroring the Android copy clarification merged in #95 so both platforms describe the "Overnight only" continuous-HRV setting identically.
What
Strand/Screens/SettingsView.swift(shared iOS + macOS) had the old vague description; this replaces it with the same wording now on Androidmain:Why it's safe / accurate
Copy-only, verbatim match to the Android string. No behaviour change — the overnight-window logic and the manual-snapshot realtime bypass are untouched. I verified both facts the copy asserts against the code:
LiveView.swift/HRVSnapshotView.swift, [sleep] Sleep & Rest #127)screenWantsRealtime = true, which arms the R-R stream regardless of the settingThe stale localized entry for the old string is simply bypassed (the code now looks up the new key; non-English falls back to the new English until re-translated) — same source-string-only scope as #95.
Closes the iOS/macOS side of #92.