feat(protocol): multi-band support — WHOOP 5 (gen5) framing + records#16
feat(protocol): multi-band support — WHOOP 5 (gen5) framing + records#16abdulsaheel wants to merge 1 commit into
Conversation
WHOOP 5 is WHOOP 4 in a different envelope: only the outer frame header
changes (gen4 4-byte + crc8 → gen5 8-byte + crc16-modbus); the payload
CRC32 and the command opcodes are shared. Introduce a BandProfile so the
frame layer carries that difference and the rest of the stack stays
band-agnostic. gen4 is the default profile, so WHOOP 4 is byte-identical
and all 2934 decode-parity cases still pass.
- crc.dart: crc16Modbus (verified 0x71E6 on the gen5 hello header)
- band.dart: DeviceType{gen4,gen5}, BandProfile, GattProfile
(gen4 6108… / gen5 fd4b…)
- framing.dart: buildFrame / parseFrame / FrameReassembler take a
BandProfile (default gen4)
- commands.dart: gen5ClientHello (GET_HELLO 0x91, reproduces the canonical
hello byte-for-byte) + gen5 empty-payload offload helpers;
profile threaded through command + history-result builders
- records.dart: parseGen5Record — thin K24 (HR@17 + timing only). Motion
(K10/K21) uses the existing R10 offsets; SpO2/temp/RR are
honest-null pending validated offsets (never fabricated)
- test: 16 gen5 cases (crc16, hello, framing round-trip, gen4
regression, gen5 reassembly, history ACK token echo, K24)
📝 WalkthroughWalkthroughAdds gen5 support across GATT profiles, CRC16-Modbus framing, command builders, streaming reassembly, historical record decoding, public exports, and end-to-end protocol tests while preserving gen4 defaults. ChangesGen5 protocol support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant BandProfile
participant buildFrame
participant FrameReassembler
participant parseFrame
BandProfile->>buildFrame: build a generation-specific header
buildFrame-->>FrameReassembler: deliver framed command or history data
FrameReassembler->>parseFrame: parse complete frame after buffering
parseFrame->>BandProfile: validate profile header and declared length
parseFrame-->>FrameReassembler: return validated inner payload
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/src/commands.dart (1)
18-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign
buildCommandwith the namedprofileconvention.
buildCommandis the only builder that makesprofilepositional, so callers have to passpayloadfirst just to override the frame profile. Make it a named parameter like the other helpers, and update thebuildHistoryResultFailcall to useprofile:too.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/src/commands.dart` around lines 18 - 30, Update buildCommand so profile is a named parameter while preserving payload as the existing positional optional argument and retaining the default BandProfile.gen4 value. Update the buildHistoryResultFail call site to pass the selected profile using the profile: named argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/src/commands.dart`:
- Around line 18-30: Update buildCommand so profile is a named parameter while
preserving payload as the existing positional optional argument and retaining
the default BandProfile.gen4 value. Update the buildHistoryResultFail call site
to pass the selected profile using the profile: named argument.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4c916b9c-27bd-496a-8453-a7691ba41f90
📒 Files selected for processing (7)
lib/openstrap_protocol.dartlib/src/band.dartlib/src/commands.dartlib/src/crc.dartlib/src/framing.dartlib/src/records.darttest/gen5_test.dart
|
Cross-checked the gen5 framing here against two independent public WHOOP RE efforts (a WHOOP 4.0 findings write-up and a WHOOP 4.0/5.0 iOS+Rust decoder) and it lines up byte-for-byte. A few things I confirmed independently:
One future surface, not blocking: packet type Nice work on this. |
Summary
Adds WHOOP 5 (gen5 / "fd4b") support to the pure-Dart protocol library. The key finding: WHOOP 5 is WHOOP 4 in a different envelope — only the outer frame header changes, so this is a small, well-bounded
BandProfileabstraction rather than a second protocol.[AA][u16 size][crc8][AA][01][u16 size][00][01][crc16-modbus]6108000x-…fd4b000x-…Changes
crc.dart—crc16Modbus(verified0x71E6on the gen5 hello header).band.dart(new) —DeviceType,BandProfile,GattProfile.framing.dart—buildFrame/parseFrame/FrameReassemblertake aBandProfile, defaulting to gen4 → WHOOP 4 byte-identical.commands.dart—gen5ClientHello()(reproduces the canonical hello byte-for-byte), gen5 empty-payload offload helpers; profile threaded through command + history-result builders.records.dart—parseGen5Record: thin K24 (HR@17 + timing). Motion uses the existing R10 offsets; SpO2/temp/RR honest-null pending validated offsets (never fabricated).Testing
dart test→ 87 pass (16 new gen5 cases + all 2934 TS-parity cases). No WHOOP 4 regression. The gen5 decode path was additionally validated end-to-end against a real WHOOP 5.0 owned capture (HR/counter/timestamp decode correctly; CRC16+CRC32 both check out).Notes
🤖 Generated with Claude Code
Summary by CodeRabbit