Skip to content

Add Whoop MG support and move BLE frame parsing to pure Swift#50

Open
naz3eh wants to merge 2 commits into
b-nnett:mainfrom
naz3eh:fix/backend
Open

Add Whoop MG support and move BLE frame parsing to pure Swift#50
naz3eh wants to merge 2 commits into
b-nnett:mainfrom
naz3eh:fix/backend

Conversation

@naz3eh

@naz3eh naz3eh commented Jun 12, 2026

Copy link
Copy Markdown

Summary

This PR adds compatibility for the Whoop MG and addresses the known performance issue by moving the per-notification frame parsing hot path from the Rust bridge to pure Swift.

WHOOP MG support

  • Device detection: new WhoopDeviceGeneration enum and a deviceGeneration / isWhoopMG property on GooseBLEClient, derived from the Device Information Service model number (already being read, so no extra BLE traffic).
  • ECG capture: when an MG is connected, physiology capture start/stop also toggles the Labrador sensor stream commands (124 data generation, 125 raw save, 139 filtered stream). Non-MG devices are unaffected.
  • K16 raw ECG packets: the Rust protocol layer now parses K16 (raw ECG Labrador) body summaries, sharing the existing R17 Labrador parsing logic. Plumbed through bridge, export, and capture correlation, with a synthetic K16 fixture and protocol tests.
  • Capture pipeline + UI: K16 frames are counted as a capture target, surfaced in health packet capture summaries ("ECG n"), and the Device view shows the MG model name and an ECG stream status row. Debug commands for each Labrador toggle are available in the advanced panel.

Performance: pure Swift frame parsing

Previously, every BLE notification batch went through protocol.parse_frame_hex_batch: hex string → JSON encode → C FFI → Rust parse → JSON encode → FFI → JSON decode, on the parse queue. This round trip was the main
source of the documented lag.

WhoopFrameParser is a drop-in replacement for NotificationFrameParser with the same parseBatch signature, producing the same NotificationFrameCompactSummary shape directly in Swift:

  • Data packets (40/43/47/51/52): packet K, counter/page, timestamps, body hex/kind, K10 embedded HR, R16/R17 flags + sample counts.
  • K10 motion: all six IMU axes at the same payload offsets as protocol.rs, with the identical motion intensity formula
    (max(rawPeakRange/32767, accVectorRange/8192) clamped to [0, 1]).
  • Events (48/53/54): event ID/name, timestamps, data hex.
  • Command and command-response packets: sequence tracking.

The Rust core is unchanged for everything else (historical sync decode, export, metrics), this only removes the bridge from the per-notification path. Output parity was checked against the Rust parser's compact summaries.

Notes

  • No changes to how the strap is paired or to any WHOOP account/subscription flow, this only reads the BLE data the strap already sends to a paired client, in line with the project's existing approach.
  • README updated to mention WHOOP MG support.

@darylbleach

Copy link
Copy Markdown

@naz3eh Are you able to add in the R22 flags? I have a packet logs from my band if that helps at all

@naz3eh

naz3eh commented Jun 12, 2026

Copy link
Copy Markdown
Author

@naz3eh Are you able to add in the R22 flags? I have a packet logs from my band if that helps at all

No, they aren't in this yet. this PR only adds MG Labrador (124/125/139 + K16/K17). R22 needs set_feature_flag_value (cmd 120) for keys like enable_r22_packets before historical sync, which we don’t send today. Packet logs from your band would help nail the exact flag set and frame layout, happy to take them in a follow up task if we want that

@darylbleach

Copy link
Copy Markdown

Untitled 2 - (null).log

These are from my band yesterday, hopefully this can help with the R22 flags

Thanks :)

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