Skip to content

v20 optical: Kotlin RawImu gate parity + restore the corroborating RE evidence (#546 follow-up) - #577

Merged
ryanbr merged 1 commit into
mainfrom
fix/v20-optical-parity-and-evidence
Jul 18, 2026
Merged

ryanbr merged 1 commit into
mainfrom
fix/v20-optical-parity-and-evidence

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Follow-up to #546 (merged). Addresses the two actionable points from its review; unlike the recent app-target PRs this is the pure WhoopProtocol package + Kotlin, so it's fully tested on both platforms (Swift 289/289, Kotlin 164/164) — no verification gap.

1. Whoop5RawImu length-gate parity (Kotlin)

#546 tightened the Swift decoder's gate >= bufferLength → == bufferLength so the 1244-B IMU and 2140-B optical decoders can't cross-misfire. The Kotlin twin still read f.size < bufferLength (i.e. accepted anything ≥ 1244), and its own KDoc already claimed "exact length" — so the code now matches both the doc and Swift:

if (f.size != bufferLength) return null   // was: f.size < bufferLength

The only caller (PuffinDeepBufferLog) already gates exact length, so this is a no-op on real frames — it just closes the same cross-misfire on Android that #546 closed on Apple. Whoop5RawImuTest stays 3/3.

2. Restore the RE evidence #546 dropped — because it corroborates the new model

#546 removed the superseded decoder's citation of issue #423's config-header fields (LED-current @28, per-photodiode offset DACs @38/@45) while upgrading "inferred to be optical" to "is optical". But those notes are the strongest independent support for the paired-block model this PR introduced. Under the 21-byte split:

#423 field lands in (block 0) meaning
LED-current @28 shared bytes @27..32 one drive current per block
photodiode DAC @38 channel-0 descriptor @33..39 one offset DAC per detector
photodiode DAC @45 channel-1 descriptor @40..46 one offset DAC per detector

One LED per block, one detector per channel — exactly what "two readout channels share one measurement config, not two wavelengths" predicts. Re-added to both Whoop5RawOptical docs (Swift + Kotlin twin) as the evidence for the detector/readout reading, so the model is self-justifying rather than asserted.

Not included

The sensor_channel_samples = max(block counts) naming point (review item 3) — a judgement call on a field name with no consumers. Left for the maintainer rather than bundled here.

Verification

  • swift test (WhoopProtocol) → 289/289
  • ./gradlew testFullDebugUnitTest --tests "com.noop.protocol.*" → 164/164

Swift change is comment-only; the Kotlin change is a stricter length gate with zero test regressions.

…ce (#546 follow-up)

Two points from the #546 review, now that it's merged.

1. Whoop5RawImu length-gate parity. #546 tightened the Swift decoder's gate from `>=` to
   `== bufferLength` so the 1244-B IMU and 2140-B optical decoders can't cross-misfire, but the
   Kotlin twin still read `f.size < bufferLength` (accepting anything >= 1244). Its own KDoc
   already claimed "exact length", so the code now matches both the doc and Swift:
   `f.size != bufferLength`. The only caller (PuffinDeepBufferLog) already gates exact length,
   so this is a no-op on real frames — it just closes the same cross-misfire the Swift change did.
   Also adds the Kotlin twin of #546's Swift over-length test (a valid buffer + 1 trailing byte
   must be rejected) — it pins the fix and fails on the old `<` gate.

2. Restore the RE evidence #546 dropped. The superseded decoder cited issue #423's config-header
   fields — an LED-current @28 and per-photodiode offset DACs @38/@45 — which #546 removed while
   strengthening "inferred to be optical" into "is optical". Those notes CORROBORATE the new
   paired-block model: under the 21-byte split, @28 falls in a block's shared bytes (@27..32) and
   @38/@45 fall in its two 7-byte channel descriptors (@33..39, @40..46) — one LED per block, one
   detector per channel, which aligns with "two readout channels share one measurement config".
   Re-added to both Whoop5RawOptical docs (Swift + Kotlin twin), hedged as a non-authoritative
   line of evidence for the detector/readout reading rather than two wavelengths.

Not included: the `sensor_channel_samples` = max(block counts) naming point (review item 3).

Tested (fully — pure package + Kotlin, no app-target/CI gap):
  swift test (WhoopProtocol) -> 289/289
  ./gradlew testFullDebugUnitTest --tests "com.noop.protocol.*" -> 164/164
@ryanbr
ryanbr force-pushed the fix/v20-optical-parity-and-evidence branch from b6e395f to 6df5b6c Compare July 18, 2026 21:22
@ryanbr
ryanbr merged commit 79600cc into main Jul 18, 2026
9 checks passed
@ryanbr
ryanbr deleted the fix/v20-optical-parity-and-evidence branch July 18, 2026 21:24
DX23876 pushed a commit to DX23876/noop that referenced this pull request Jul 22, 2026
…ce (ryanbr#546 follow-up) (ryanbr#577)

Two points from the ryanbr#546 review, now that it's merged.

1. Whoop5RawImu length-gate parity. ryanbr#546 tightened the Swift decoder's gate from `>=` to
   `== bufferLength` so the 1244-B IMU and 2140-B optical decoders can't cross-misfire, but the
   Kotlin twin still read `f.size < bufferLength` (accepting anything >= 1244). Its own KDoc
   already claimed "exact length", so the code now matches both the doc and Swift:
   `f.size != bufferLength`. The only caller (PuffinDeepBufferLog) already gates exact length,
   so this is a no-op on real frames — it just closes the same cross-misfire the Swift change did.
   Also adds the Kotlin twin of ryanbr#546's Swift over-length test (a valid buffer + 1 trailing byte
   must be rejected) — it pins the fix and fails on the old `<` gate.

2. Restore the RE evidence ryanbr#546 dropped. The superseded decoder cited issue ryanbr#423's config-header
   fields — an LED-current @28 and per-photodiode offset DACs @38/@45 — which ryanbr#546 removed while
   strengthening "inferred to be optical" into "is optical". Those notes CORROBORATE the new
   paired-block model: under the 21-byte split, @28 falls in a block's shared bytes (@27..32) and
   @38/@45 fall in its two 7-byte channel descriptors (@33..39, @40..46) — one LED per block, one
   detector per channel, which aligns with "two readout channels share one measurement config".
   Re-added to both Whoop5RawOptical docs (Swift + Kotlin twin), hedged as a non-authoritative
   line of evidence for the detector/readout reading rather than two wavelengths.

Not included: the `sensor_channel_samples` = max(block counts) naming point (review item 3).

Tested (fully — pure package + Kotlin, no app-target/CI gap):
  swift test (WhoopProtocol) -> 289/289
  ./gradlew testFullDebugUnitTest --tests "com.noop.protocol.*" -> 164/164
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…ce (ryanbr#546 follow-up) (ryanbr#577)

Two points from the ryanbr#546 review, now that it's merged.

1. Whoop5RawImu length-gate parity. ryanbr#546 tightened the Swift decoder's gate from `>=` to
   `== bufferLength` so the 1244-B IMU and 2140-B optical decoders can't cross-misfire, but the
   Kotlin twin still read `f.size < bufferLength` (accepting anything >= 1244). Its own KDoc
   already claimed "exact length", so the code now matches both the doc and Swift:
   `f.size != bufferLength`. The only caller (PuffinDeepBufferLog) already gates exact length,
   so this is a no-op on real frames — it just closes the same cross-misfire the Swift change did.
   Also adds the Kotlin twin of ryanbr#546's Swift over-length test (a valid buffer + 1 trailing byte
   must be rejected) — it pins the fix and fails on the old `<` gate.

2. Restore the RE evidence ryanbr#546 dropped. The superseded decoder cited issue ryanbr#423's config-header
   fields — an LED-current @28 and per-photodiode offset DACs @38/@45 — which ryanbr#546 removed while
   strengthening "inferred to be optical" into "is optical". Those notes CORROBORATE the new
   paired-block model: under the 21-byte split, @28 falls in a block's shared bytes (@27..32) and
   @38/@45 fall in its two 7-byte channel descriptors (@33..39, @40..46) — one LED per block, one
   detector per channel, which aligns with "two readout channels share one measurement config".
   Re-added to both Whoop5RawOptical docs (Swift + Kotlin twin), hedged as a non-authoritative
   line of evidence for the detector/readout reading rather than two wavelengths.

Not included: the `sensor_channel_samples` = max(block counts) naming point (review item 3).

Tested (fully — pure package + Kotlin, no app-target/CI gap):
  swift test (WhoopProtocol) -> 289/289
  ./gradlew testFullDebugUnitTest --tests "com.noop.protocol.*" -> 164/164
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant