Skip to content

protocol(mg): decode the type-43 REALTIME_RAW_DATA record on both platforms - #1765

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
Zebsi235:feat/whoop5-ecg-raw-decoder
Aug 31, 2026
Merged

protocol(mg): decode the type-43 REALTIME_RAW_DATA record on both platforms#1765
ryanbr merged 1 commit into
ryanbr:mainfrom
Zebsi235:feat/whoop5-ecg-raw-decoder

Conversation

@Zebsi235

Copy link
Copy Markdown

What this PR does

Adds a decoder for the 240-byte type-43 REALTIME_RAW_DATA record — the packet the MG streams once the #1727 turn-on order (139 = 1 then 124 = 2) has opened the stream. Pure protocol on both platforms: no transport, no UI, no storage, no enum changes, and nothing sent to a strap.

The layout, observed across 315 records in one session on a WHOOP MG (WS50_r00, fw 50.39.1.0):

@0-7      frame header ([8] is the inner record's type byte on 5/MG)
@24-33    constant 5x i16 sub-header — NOT waveform
@34-235   101 samples, i16 little-endian
@236-239  CRC32 trailer

What it exposes: isRealtimeRawRecord, realtimeRawSamples, realtimeRawBodyNonZeroBytes, and one realtimeRawSignalPresent rule, plus the named offsets.

Why it belongs in the protocol layer

Three consumers need this record — a live view, a signal classifier, a waveform export — and in my working tree each had grown its own copy with magic offsets 8/24/34/236 and a bare 43. Two of them had drifted to different definitions of "signal present": one counted non-zero samples, the other non-zero body bytes. Neither could be tested without a strap.

Putting it here with the other Labrador facts means the copies cannot disagree, and the decode gets unit tests that need no hardware. That is the whole PR — the consumers are separate work and none of them is in this diff.

What it does not claim

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / tooling

How it was tested

Not on the BLE path. Everything added is a pure function over a byte array.

Six mirrored tests per platform (Whoop5EcgRawRecordTest / Whoop5EcgRawRecordTests), pinning:

  • the record carries exactly 101 samples
  • only a 240-byte type-43 frame is a raw record
  • samples are signed little-endian
  • zero samples are kept, not trimmed — trimming would silently edit the evidence
  • signalPresent is one rule shared by every consumer, asserted at its boundary
  • the sub-header is counted for fill but never decoded as waveform

./gradlew testFullDebugUnitTest --rerun-tasks --no-build-cache on Windows 11 / JDK 17 against main @ 9991f7b: 4,959 tests across 606 classes, 0 failures, 0 errors (6 skipped, pre-existing). Whoop5EcgRawRecordTest 6/6, and the neighbouring Whoop5EcgTest (38) and Whoop5EcgProbeTest (20) unchanged and green.

python Tools/doc_comment_lint.py clean. python Tools/i18n_audit.py --ci exit 0.

The gap I would rather state than have you find: the Swift half is not run locally, no Apple machine. Whoop5EcgRawRecordTests lives in Packages/WhoopProtocol, which swift-packages.yml covers. The Swift decoder mirrors the Kotlin one and its tests pin the same six properties, but that is reasoning plus CI rather than a local run — the same declared gap that CI correctly caught on #1727.

Checklist

  • Swift package tests pass — not run locally (no Apple machine); left to swift-packages.yml
  • Android unit tests pass (./gradlew testFullDebugUnitTest, full suite)
  • No new build warnings introduced
  • UI changes use only StrandDesign tokens — n/a, no UI
  • No hardcoded hex frame bytes; protocol facts live in the schema / decoders — this PR is that consolidation
  • Follows the conventions in docs/CONTRIBUTING.md
  • I did not commit generated output or any secrets/keystores

Related issues

Refs #891 and #1100, whose turn-on sequence produces these records, and #1727, which corrected the argument that starts them.

Closes nothing. This is the decode only; what reads it is separate work I would rather propose one piece at a time.

…tforms

The 240-byte type-43 record is the live sample carrier the ryanbr#891/ryanbr#1100
turn-on sequence produces. Its layout was observed on one MG (WS50_r00,
fw 50.39.1.0): a constant sub-header at 24..33, 101 i16-LE samples at
34..235, CRC32 trailer. The decoder lives with the other Labrador
protocol facts so the consumers to come cannot drift apart, exposes one
signal-present rule, and is pure on both platforms with mirrored tests
pinning the layout, sign handling, the sample count, and that the
sub-header is counted for fill but never decoded as waveform.
@ryanbr
ryanbr merged commit c40287f into ryanbr:main Aug 31, 2026
15 checks passed
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.

2 participants