Repository navigation
fix(sleep): displaySmoothed(minDuration<=0) coalesces like Swift instead of returning raw fragments (#240 follow-up) - #316
Merged
Conversation
… not return raw fragments (#240 follow-up) The Android port of Swift Hypnogram.displaySmoothed added an early return `if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The `|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port is faithful. Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port" claim true and protects a future 0-caller. Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem] fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced result — verified it FAILS against the old short-circuit and passes with the fix. Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
… not return raw fragments (ryanbr#240 follow-up) (ryanbr#316) The Android port of Swift Hypnogram.displaySmoothed added an early return `if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The `|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port is faithful. Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port" claim true and protects a future 0-caller. Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem] fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced result — verified it FAILS against the old short-circuit and passes with the fix. Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
ryanbr
added a commit
that referenced
this pull request
Jul 26, 2026
Swept their decoder against every v18 byte we still carry as raw, rather than only the ones they advertise in their README. Three results. @40 gets a name. They decode it as `signal_quality`. Plausible for a byte sitting beside the HR fields, but it appears only in their code — no README entry, no source tag, no supporting analysis — so it stays raw here and is recorded as a lead rather than adopted. Noting it so the next person looking at that byte starts with a hypothesis instead of nothing. @63 is corroborated. They decode the same offset as an activity class with the same `<= 2` gate we use (#316), reached independently. That is a second source on a field we had from one. The SpO2 gate is evidence. Their code clamps byte 82 to 70..100 and drops anything outside instead of storing it unconditionally. Code shaped that way implies its author saw values inside the band and values outside it worth rejecting — which is what a real percentage with invalid samples looks like, and not what a 0-3 state code looks like. Recorded as suggestive, not decisive: a gate can also be purely defensive. Nothing adopted, nothing decoded. Their remaining unmatched offsets checked out as coincidental number collisions, not claims. Deliberately NOT touching the Credits table: it documents projects whose findings this codebase PORTS, and we have ported nothing from whoop-local — the inline #715 citations are the accurate form of credit here.
ryanbr
added a commit
that referenced
this pull request
Jul 26, 2026
…ion claim, record the byte-82 lead (#520, #715) Docs and test fixture only. No decoder change, no schema, no scores, no strings. BYTE 43 IS NOT A RESPIRATION RATE (#520) A third-party decoder reads a u8 at payload offset 35 — frame offset 43 — as a raw respiration rate. It is the third byte of the little-endian dynamic_acceleration float32 at 41. Disproved against the six real v18 frames already in the repo: frame byte43 as "brpm" f32@41 (g) |g| @45/49/53 whoop5_v18_real_worn 22 0.0092 1.0086 whoop5_v18_real_one_rr 47 0.0107 1.0106 whoop5_v18_real_offwrist 195 0.0060 0.9985 whoop5_v18_real_ack_capture 108 0.0144 1.0074 whoop5_v18_real_device2_hr57 6 0.0328 1.0029 whoop5_v18_real_device2_hr63 9 0.0084 1.0096 The evidence in order: the f32s at 45/49/53 give |g| = 0.9985..1.0106 on every frame, which a misaligned read does not produce, so the 4-byte grid is fixed; read as an f32 on that grid, 41-44 is physically coherent across worn, off-wrist and two straps; read as a rate, byte 43 is impossible — worst on the OFF-WRIST frame, 195 breaths/min from a strap nobody is wearing with heart_rate@22 = 0. A byte belongs to one field. The doc states the tautology to avoid: byte 43 IS byte 2 of that float by construction, so reproducing it from the exponent/mantissa bits proves nothing. The argument is the gravity anchor plus the off-wrist value. dynamic_acceleration is now pinned in decoder_oracle.json on both platforms, so a future offset change fails a test rather than silently reintroducing a fabricated vital sign. No new test code needed — both harnesses already assert arbitrary double keys within tolerance, and testOracleCopiesAreIdentical keeps the copies in sync. Also adds the @41 row to the v18 table. The field has been decoded on both platforms all along but had no entry in the spec, which is part of why an outside reader could mistake one of its bytes for a channel of its own. BYTE 82 MAY BE A SLEEP-ONLY SpO2 % (#715) — OPEN whoop-local reads byte 82 as WHOOP's own computed SpO2 percentage, sleep-only, attributing it to hardware RE rather than app decompilation. That fits an observation already in our table and never explained: aux_byte_82 is nonzero only while sleep_state = asleep. It cannot be confirmed here, and the evidence base is thinner than a grep suggests: there are six DISTINCT v18 frames in the tree, from two straps, recurring across 2-7 files each because the Swift and Kotlin suites share fixtures — a naive count returns 21 and overstates the sample 3.5x. All six are awake with byte 82 = 0, which is consistent with a sleep-only channel and with several other readings. Recorded as an open hypothesis; the byte stays raw and unnamed. One asleep record settles it: 90-100 is a percentage, 0-3 a state code. The doc also names the likely shortcut — the out-of-tree ~258k-record corpus this file cites elsewhere for hr_fixed_8_8@36 and for status_word@75 "occurring as often awake as asleep" must contain asleep records, so one query against it resolves this without a new capture. CROSS-CHECK FINDINGS v20 optical, independently corroborated: whoop-local lists six 20-bit channel offsets derived separately — frame 47, 247, 1313, 1513, 1735, 1935 — which are exactly the six slots of our non-empty blocks (0, 3, 4; the corpus shows counts are always [25, 0, 0, 25, 25]) out of the ten this document lists. Two unrelated methods, same six. The doc says explicitly that this confirms STRUCTURE only — neither project has a labelled capture, so the v20 sensor-identity question is untouched. @63 corroborated: they decode the same offset as an activity class with the same <= 2 gate we use (#316), reached independently. @40 gets a name: they call it signal_quality. Plausible beside the HR fields, but it appears only in their code with no source tag and no analysis, so it stays raw here and is recorded as a lead. Their SpO2 gate is itself evidence: the code clamps byte 82 to 70..100 and drops anything outside rather than storing it, which implies the author saw values inside that band and values outside worth rejecting. Suggestive, not decisive. Nothing was adopted from whoop-local. Its primary source is the decompiled WHOOP Android app (com/whoop/service/rearchitect/c.java, zi0/*, bj0/x.java, qh0/*), which this project's clean-room rule rules out regardless of correctness. The findings used here are the ones it attributes to hardware RE, and each is recorded as a lead to verify rather than a fact to copy. The Credits table is deliberately untouched: it documents projects whose findings this codebase PORTS, and nothing was ported.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
… not return raw fragments (ryanbr#240 follow-up) (ryanbr#316) The Android port of Swift Hypnogram.displaySmoothed added an early return `if (intervals.size <= 2 || minDurationSec <= 0.0) return intervals`. The `|| minDurationSec <= 0.0` short-circuit isn't in Swift, which guards ONLY on count and lets the "raw" (minDuration 0) case fall through to coalesce() — so Swift returns the coalesced timeline, while Android returned the 60-100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port is faithful. Production is unaffected (the only caller passes STAGE_ROW_SMOOTH_SEC = 90.0), so this is a latent port-fidelity fix, not a shipping bug — but it makes the "straight port" claim true and protects a future 0-caller. Also strengthens `zeroFloorReturnsCoalescedInputUnsmoothed`: its old [light, deep, rem] fixture had no adjacent same-stage runs, so coalesce was a no-op and it passed even with the short-circuit. New fixture has a light/light seam and asserts the coalesced result — verified it FAILS against the old short-circuit and passes with the fix. Android-only (Health Connect / SleepScreen); testFullDebugUnitTest green.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ion claim, record the byte-82 lead (ryanbr#520, ryanbr#715) Docs and test fixture only. No decoder change, no schema, no scores, no strings. BYTE 43 IS NOT A RESPIRATION RATE (ryanbr#520) A third-party decoder reads a u8 at payload offset 35 — frame offset 43 — as a raw respiration rate. It is the third byte of the little-endian dynamic_acceleration float32 at 41. Disproved against the six real v18 frames already in the repo: frame byte43 as "brpm" f32@41 (g) |g| @45/49/53 whoop5_v18_real_worn 22 0.0092 1.0086 whoop5_v18_real_one_rr 47 0.0107 1.0106 whoop5_v18_real_offwrist 195 0.0060 0.9985 whoop5_v18_real_ack_capture 108 0.0144 1.0074 whoop5_v18_real_device2_hr57 6 0.0328 1.0029 whoop5_v18_real_device2_hr63 9 0.0084 1.0096 The evidence in order: the f32s at 45/49/53 give |g| = 0.9985..1.0106 on every frame, which a misaligned read does not produce, so the 4-byte grid is fixed; read as an f32 on that grid, 41-44 is physically coherent across worn, off-wrist and two straps; read as a rate, byte 43 is impossible — worst on the OFF-WRIST frame, 195 breaths/min from a strap nobody is wearing with heart_rate@22 = 0. A byte belongs to one field. The doc states the tautology to avoid: byte 43 IS byte 2 of that float by construction, so reproducing it from the exponent/mantissa bits proves nothing. The argument is the gravity anchor plus the off-wrist value. dynamic_acceleration is now pinned in decoder_oracle.json on both platforms, so a future offset change fails a test rather than silently reintroducing a fabricated vital sign. No new test code needed — both harnesses already assert arbitrary double keys within tolerance, and testOracleCopiesAreIdentical keeps the copies in sync. Also adds the @41 row to the v18 table. The field has been decoded on both platforms all along but had no entry in the spec, which is part of why an outside reader could mistake one of its bytes for a channel of its own. BYTE 82 MAY BE A SLEEP-ONLY SpO2 % (ryanbr#715) — OPEN whoop-local reads byte 82 as WHOOP's own computed SpO2 percentage, sleep-only, attributing it to hardware RE rather than app decompilation. That fits an observation already in our table and never explained: aux_byte_82 is nonzero only while sleep_state = asleep. It cannot be confirmed here, and the evidence base is thinner than a grep suggests: there are six DISTINCT v18 frames in the tree, from two straps, recurring across 2-7 files each because the Swift and Kotlin suites share fixtures — a naive count returns 21 and overstates the sample 3.5x. All six are awake with byte 82 = 0, which is consistent with a sleep-only channel and with several other readings. Recorded as an open hypothesis; the byte stays raw and unnamed. One asleep record settles it: 90-100 is a percentage, 0-3 a state code. The doc also names the likely shortcut — the out-of-tree ~258k-record corpus this file cites elsewhere for hr_fixed_8_8@36 and for status_word@75 "occurring as often awake as asleep" must contain asleep records, so one query against it resolves this without a new capture. CROSS-CHECK FINDINGS v20 optical, independently corroborated: whoop-local lists six 20-bit channel offsets derived separately — frame 47, 247, 1313, 1513, 1735, 1935 — which are exactly the six slots of our non-empty blocks (0, 3, 4; the corpus shows counts are always [25, 0, 0, 25, 25]) out of the ten this document lists. Two unrelated methods, same six. The doc says explicitly that this confirms STRUCTURE only — neither project has a labelled capture, so the v20 sensor-identity question is untouched. @63 corroborated: they decode the same offset as an activity class with the same <= 2 gate we use (ryanbr#316), reached independently. @40 gets a name: they call it signal_quality. Plausible beside the HR fields, but it appears only in their code with no source tag and no analysis, so it stays raw here and is recorded as a lead. Their SpO2 gate is itself evidence: the code clamps byte 82 to 70..100 and drops anything outside rather than storing it, which implies the author saw values inside that band and values outside worth rejecting. Suggestive, not decisive. Nothing was adopted from whoop-local. Its primary source is the decompiled WHOOP Android app (com/whoop/service/rearchitect/c.java, zi0/*, bj0/x.java, qh0/*), which this project's clean-room rule rules out regardless of correctness. The findings used here are the ones it attributes to hardware RE, and each is recorded as a lead to verify rather than a fact to copy. The Credits table is deliberately untouched: it documents projects whose findings this codebase PORTS, and nothing was ported.
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.
Follow-up to #240 (merged). Fixes the one port-fidelity divergence the review found.
The divergence
The Android port of Swift
Hypnogram.displaySmoothedaddedif (intervals.size <= 2 || minDurationSec <= 0.0) return intervals. Swift guards only on count and letsminDuration = 0("raw") fall through tocoalesce()— so Swift returns the coalesced timeline, whereas Android returned the 60–100 un-merged epoch fragments (the "comb" the smoothing exists to remove). Everything else in the port matches Swift line-for-line.Severity
Production-inert: the only caller passes
STAGE_ROW_SMOOTH_SEC = 90.0, so the divergent<= 0path never runs in shipping. This is a latent fidelity fix — it makes the "straight port" true and protects any future0-caller.Fix
|| minDurationSec <= 0.0short-circuit. WithminDuration = 0, the absorb loop'sduration < 0filter is empty, so it breaks right after the firstcoalesce()— exactly Swift's result.zeroFloorReturnsCoalescedInputUnsmoothedpreviously used[light, deep, rem](no adjacent same-stage →coalescea no-op), so it passed even with the short-circuit. New fixture has alight/lightseam and asserts the coalesced output. Verified sensitive: it fails against the old short-circuit, passes with the fix.Android-only;
testFullDebugUnitTestgreen.