You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds a decoder for the unframed 96-byte realtime sensor packet that WHOOP 4.0 delivers as raw BLE notifications on the data-stream characteristic (distinct from the framed 0xAA… packets handled by framing.dart).
lib/src/realtime96.dart: parseRealtime96 decodes sequence (@0), hr×100 (u16 LE @1), RR ms (u16 LE @3), SpO2 byte 5 as raw only, skin-temp byte 6 raw, and validates the CRC-32 trailer (@92).
crc32WhoopStream: reflected CRC-32 with xor-out 0xF43F44AC for this stream — deliberately different from the framed-packet crc32 (zlib xor-out 0xFFFFFFFF); two independent community sources agree on this xor-out.
Exported via openstrap_protocol.dart (Realtime96, parseRealtime96, crc32WhoopStream, kRealtime96PacketSize).
Honesty contract (why SpO2 stays raw)
Byte 5 is a single-source community reverse-engineering finding with only a 50–100 range gate, no ground truth; the original RE write-up says blood oxygen on this band is measured only during sleep. Therefore the decoder exposes spo2Raw exactly as received — no plausibility gate, no percentage getter, nothing user-facing. Values >128 pass through unmasked so captures can be correlated later.
Review-driven changes (what was reworked and why)
CodeRabbit: the CRC test was self-referential (fixture built with the same function under test) → added an independent known-answer vector: crc32WhoopStream("123456789") == 0xC0348275 (verified independently: standard CRC-32 of "123456789" is 0xCBF43926; xor-out difference yields 0xC0348275). Commit 0702595.
Tests
test/realtime96_test.dart: field decode, length gate (≠96 → null), corrupted payload → crcValid == false with raw fields intact, raw passthrough incl. 0xFF, stream-CRC vs framed-CRC distinction + known-answer vector.
Still to do (open items, explicit)
Run the tests once for realDONE locally on Windows (Dart 3.11.4): dart analyze clean, dart test test/realtime96_test.dart → All tests passed (5/5).
Wire-validate the CRC xor-out (0xF43F44AC) against a real 96-byte capture from an actual band — community-sourced so far; arithmetic is pinned by the known-answer vector, the wire fact is not.
Resolve the characteristic-UUID discrepancy (61080004 per whoop-reader vs 61080005 per whoop-rs uuids.rs) from real notifications before anything consumes this stream in edge.
Real-frame fixtures: CONTRIBUTING asks for test cases from real frames for new record types — current tests are synthetic (layout + known-answer CRC only); add a real captured 96-byte packet when available.
No consumer wired yet: edge does not subscribe/decode this stream; any live use stays RAM-only per edge AGENTS.md invariant 14 and needs ground-truth validation (see the edge proposal PR) before anything user-facing.
Verification status (honest)
CodeRabbit review (on fork PR): pass, no open comments.
CI: no test job exists for protocol PRs; CodeRabbit pass only.
Local Windows run: dart pub get ok, dart analyze → No issues found!, dart test test/realtime96_test.dart → +5: All tests passed!.
Remaining caveat: the CRC xor-out still needs wire-validation against a real capture.
Summary by Sourcery
Add support for parsing unframed 96-byte WHOOP realtime sensor notifications while preserving unverified measurements as raw data.
New Features:
Add decoding and public exports for unframed 96-byte WHOOP realtime sensor packets, including raw sensor fields and CRC status.
Enhancements:
Introduce the stream-specific CRC-32 implementation with an independently verified known-answer vector and keep uncertain SpO2 and temperature values uncalibrated and raw.
Tests:
Add coverage for field decoding, exact packet lengths, CRC failures, raw SpO2 passthrough, byte normalization, and distinction from the framed-packet CRC.
Summary by CodeRabbit
New Features
Added support for decoding unframed 96-byte realtime sensor packets, including sequence, heart rate, RR interval, SpO₂, and skin-temperature readings.
Decoded readings include an indicator of whether the packet’s CRC check passed. Packets with an invalid CRC can still return decoded values.
Added support for calculating the stream-specific CRC-32 checksum.
Inputs that are not exactly 96 bytes are rejected. SpO₂ and skin-temperature values are provided in raw form, without calibrated measurements.
… CRC32)
Adds parseRealtime96/crc32WhoopStream for the raw data-stream
characteristic. SpO2 byte 5 is exposed raw only — unvalidated community
finding, never published as a percentage.
Co-authored-by: BucciMobile <BucciMobile@users.noreply.github.com>
Define the packet layout and the public raw-field structure that decoded realtime packets expose.
lib/src/realtime96.dart L1-17, L52-93
API
Expose the realtime decoder and stream CRC publicly, and implement the callable CRC and packet-parsing interfaces.
lib/openstrap_protocol.dart L102-105
lib/src/realtime96.dart L24-37, L94-111
Implementation
Normalize decoder inputs and build the lookup table used by the stream-specific CRC implementation.
lib/src/realtime96.dart L18-23, L38-51
Tests
Exercise raw field decoding, length and CRC handling, passthrough behavior, and an independent CRC known-answer vector.
test/realtime96_test.dart L1-85
Tips and commands
Interacting with Sourcery
Trigger a new review: Comment @sourcery-ai review on the pull request.
Continue discussions: Reply directly to Sourcery's review comments.
Generate a GitHub issue from a review comment: Ask Sourcery to create an
issue from a review comment by replying to it. You can also reply to a
review comment with @sourcery-ai issue to create an issue from it.
Generate a pull request title: Write @sourcery-ai anywhere in the pull
request title to generate a title at any time. You can also comment @sourcery-ai title on the pull request to (re-)generate the title at any time.
Generate a pull request summary: Write @sourcery-ai summary anywhere in
the pull request body to generate a PR summary at any time exactly where you
want it. You can also comment @sourcery-ai summary on the pull request to
(re-)generate the summary at any time.
Generate reviewer's guide: Comment @sourcery-ai guide on the pull
request to (re-)generate the reviewer's guide at any time.
Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
pull request to resolve all Sourcery comments. Useful if you've already
addressed all the comments and don't want to see them anymore.
Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
request to dismiss all existing Sourcery reviews. Especially useful if you
want to start fresh with a new review - don't forget to comment @sourcery-ai review to trigger a new review!
No actionable comments were generated in the recent review.
Walkthrough
The package now exports an API for parsing unframed 96-byte realtime sensor packets. The parser exposes raw sensor fields and reports CRC validity. It returns null when the input length is not 96 bytes.
Changes: Realtime packet API
Layer / File(s)
Summary
Packet decoding and public API lib/src/realtime96.dart, lib/openstrap_protocol.dart, test/realtime96_test.dart
Adds the packet model, a stream-specific CRC-32 function, and a parser that returns raw fields with a CRC-validity flag. Exports the API and tests packet lengths, decoded fields, CRC behavior, and raw SpO₂ values.
The decoder is mergeable after normal checks. Its CRC assumption should be validated against a real packet when one becomes available.
Pre-merge checks: 5
Check name
Status
Explanation
Description Check
Check skipped - CodeRabbit’s high-level summary is enabled.
Title check
The title clearly summarizes the main change: decoding unframed 96-byte realtime packets and exposing the raw SpO₂ byte and stream CRC-32.
Docstring Coverage
No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage
Explanation
No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 0 files. (3 skipped: 3 unsupported.)
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: 436fad4f-f9db-4e81-8117-8110ee369f98
Commits
Reviewing files that changed from the base of the PR and between b4fde6a and c896c9a.
Files selected for processing: 3
lib/openstrap_protocol.dart
lib/src/realtime96.dart
test/realtime96_test.dart
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
The reason will be displayed to describe this comment to others. Learn more.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments### Comment 1
<locationpath="lib/src/realtime96.dart"line_range="35" />
<code_context>
+ for (final b in data) {
+ crc = _crc32Table[(crc ^ b) & 0xFF] ^ (crc >>> 8);
+ }
+ return (crc ^ 0xF43F44AC) & 0xFFFFFFFF;
+}
+
</code_context>
<issue_to_address>
**Valid packets fail CRC validation**
When a real band capture uses a CRC xor-out other than `0xF43F44AC`, `crc32WhoopStream` applies this xor-out to every packet checksum, so if the band uses a different wire xor-out, `parseRealtime96` marks every intact packet `crcValid == false` and callers that rely on this flag reject the data.
Verify the xor-out against a real 96-byte capture and update the implementation and fixture if it differs.
</issue_to_address>
### Comment 2
<locationpath="lib/src/realtime96.dart"line_range="103-106" />
<code_context>
+ final view = ByteData.sublistView(bytes is Uint8List ? bytes : Uint8List.fromList(bytes));
+ final crcReceived = view.getUint32(92, Endian.little);
+ return Realtime96(
+ sequence: bytes[0],
+ hrTimes100: view.getUint16(1, Endian.little),
+ rrMsRaw: view.getUint16(3, Endian.little),
+ spo2Raw: bytes[5],
+ skinTempRaw: bytes[6],
+ crcValid: crc32WhoopStream(bytes.sublist(0, 92)) == crcReceived,
</code_context>
<issue_to_address>
**Decoded fields disagree with packet bytes**
When a caller passes a 96-element `List<int>` containing values outside 0–255, `view` and `crc32WhoopStream` use the octet-normalized copy, but `sequence`, `spo2Raw` and `skinTempRaw` read the original list. The parser can accept a packet whose fields disagree, exposing values such as 256 or -1 to callers.
Normalize the input once and use the normalized bytes for all field reads and CRC validation.
</issue_to_address>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium · Valid packets fail CRC validation
When a real band capture uses a CRC xor-out other than 0xF43F44AC, crc32WhoopStream applies this xor-out to every packet checksum, so if the band uses a different wire xor-out, parseRealtime96 marks every intact packet crcValid == false and callers that rely on this flag reject the data.
Verify the xor-out against a real 96-byte capture and update the implementation and fixture if it differs.
Prompt for AI agents
In `lib/src/realtime96.dart` at line 35:
**Valid packets fail CRC validation**
When a real band capture uses a CRC xor-out other than `0xF43F44AC`, `crc32WhoopStream` applies this xor-out to every packet checksum, so if the band uses a different wire xor-out, `parseRealtime96` marks every intact packet `crcValid == false` and callers that rely on this flag reject the data.
Verify the xor-out against a real 96-byte capture and update the implementation and fixture if it differs.
The reason will be displayed to describe this comment to others. Learn more.
Additional comments: 2
lib/src/realtime96.dart-25-25: 1
25-25: Trivial · Maintainability & Code Quality · Low value
Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.
Fix the CRC doc comment: the polynomial and bit order are inconsistent.
The comment names the polynomial as 0x04C11DB7 and also calls the CRC reflected. The implementation uses the reflected constant 0xEDB88320 (Line 45), which is the reflected form of 0x04C11DB7. The two are consistent, but the comment can mislead a reader. Say that the polynomial is 0x04C11DB7 and that the code uses its reflected form 0xEDB88320.
I checked the known-answer vector by hand. CRC-32 of "123456789" is 0xCBF43926. Its pre-final-XOR register is 0x340BC6D9. XOR with 0xF43F44AC gives 0xC0348275, which matches the test.
lib/src/realtime96.dart-108-108: 1
108-108: Trivial · Performance & Scalability · Low value
Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.
Avoid the extra copy in the CRC check.
bytes.sublist(0, 92) allocates a new list for every packet. Packets arrive as BLE notifications, so this runs at the stream rate. Give crc32WhoopStream an optional end parameter, or pass a Uint8List.sublistView. This removes the allocation.
Review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: 0e213991-c3da-468f-91db-e9779df38cbb
Commits
Reviewing files that changed from the base of the PR and between b4fde6a and 0702595.
Files selected for processing: 3
lib/openstrap_protocol.dart
lib/src/realtime96.dart
test/realtime96_test.dart
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
The reason will be displayed to describe this comment to others. Learn more.
Additional comments: 1
lib/src/realtime96.dart-25-25: 1
25-25: Trivial · Maintainability & Code Quality · Low value
Unverified finding
Verification ran but could not confirm this finding. It is shown for review, not as a verified issue.
Correct the CRC polynomial description in the doc comment.
The comment says "poly 0x04C11DB7". The table at Line 45 uses 0xEDB88320. This is the bit-reversed form of the same polynomial, which is what a reflected CRC-32 requires. The comment is not wrong, but it is easy to misread. Add the reflected constant so the doc matches the code.
📝 Proposed fix
-/// Reflected CRC-32 (poly 0x04C11DB7, init 0xFFFFFFFF) with xor-out+/// Reflected CRC-32 (poly 0x04C11DB7, reflected form 0xEDB88320, init 0xFFFFFFFF) with xor-out
Review info
Run configuration
Source mode: Auto
Repository: Unavailable
Branch: Unavailable
File: No configuration file from this repository applied
Commit: Unavailable
Layers: Organization UI
Review profile: ASSERTIVE
Plan: Advanced
Run ID: 970e6300-fa4b-4c16-9279-f7d32550c09d
Commits
Reviewing files that changed from the base of the PR and between b4fde6a and 0702595.
Files selected for processing: 3
lib/openstrap_protocol.dart
lib/src/realtime96.dart
test/realtime96_test.dart
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.
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
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.
Summary
Adds a decoder for the unframed 96-byte realtime sensor packet that WHOOP 4.0 delivers as raw BLE notifications on the data-stream characteristic (distinct from the framed
0xAA…packets handled byframing.dart).lib/src/realtime96.dart:parseRealtime96decodes sequence (@0), hr×100 (u16 LE @1), RR ms (u16 LE @3), SpO2 byte 5 as raw only, skin-temp byte 6 raw, and validates the CRC-32 trailer (@92).crc32WhoopStream: reflected CRC-32 with xor-out0xF43F44ACfor this stream — deliberately different from the framed-packetcrc32(zlib xor-out0xFFFFFFFF); two independent community sources agree on this xor-out.openstrap_protocol.dart(Realtime96,parseRealtime96,crc32WhoopStream,kRealtime96PacketSize).Honesty contract (why SpO2 stays raw)
Byte 5 is a single-source community reverse-engineering finding with only a 50–100 range gate, no ground truth; the original RE write-up says blood oxygen on this band is measured only during sleep. Therefore the decoder exposes
spo2Rawexactly as received — no plausibility gate, no percentage getter, nothing user-facing. Values >128 pass through unmasked so captures can be correlated later.Review-driven changes (what was reworked and why)
crc32WhoopStream("123456789") == 0xC0348275(verified independently: standard CRC-32 of "123456789" is0xCBF43926; xor-out difference yields0xC0348275). Commit0702595.Tests
test/realtime96_test.dart: field decode, length gate (≠96 → null), corrupted payload →crcValid == falsewith raw fields intact, raw passthrough incl.0xFF, stream-CRC vs framed-CRC distinction + known-answer vector.Still to do (open items, explicit)
Run the tests once for realDONE locally on Windows (Dart 3.11.4):dart analyzeclean,dart test test/realtime96_test.dart→ All tests passed (5/5).0xF43F44AC) against a real 96-byte capture from an actual band — community-sourced so far; arithmetic is pinned by the known-answer vector, the wire fact is not.61080004per whoop-reader vs61080005per whoop-rsuuids.rs) from real notifications before anything consumes this stream in edge.Verification status (honest)
dart pub getok,dart analyze→ No issues found!,dart test test/realtime96_test.dart→+5: All tests passed!.Summary by Sourcery
Add support for parsing unframed 96-byte WHOOP realtime sensor notifications while preserving unverified measurements as raw data.
New Features:
Enhancements:
Tests:
Summary by CodeRabbit