Skip to content

Support direct heart-rate broadcasting on WHOOP 4.0 - #2400

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
don86nl:feat/whoop4-direct-hr-broadcast
Sep 23, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
don86nl:feat/whoop4-direct-hr-broadcast

Conversation

@don86nl

@don86nl don86nl commented Sep 22, 2026

Copy link
Copy Markdown

Summary

  • add the reversible WHOOP 4.0 TOGGLE_GENERIC_HR_PROFILE (14) path alongside the existing 5.0/MG device-config path
  • expose direct strap broadcasting in Data Sources for both generations
  • restore an opted-in WHOOP 4.0 broadcast after reconnect and pin both frame shapes in Android and Swift tests
  • correct the 4.0/5.0 capability copy

Refs #159

Verification

  • Android compile: ./gradlew :app:compileFullDebugKotlin
  • Android targeted tests: ./gradlew :app:testFullDebugUnitTest --tests com.noop.protocol.BroadcastHrConfigTest
  • WHOOP 4.0 hardware: enabled and disabled direct strap broadcast successfully, with both transitions observed
  • i18n diff gate and doc-comment lint pass
  • Swift implementation and exact-frame tests were added for parity; they were not locally compiled because the development host is Windows

@ryanbr ryanbr left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @don86nl. The shape is right: opcode 14 is free in CommandNumber (the DOUBLE_TAP(14) clash is in the event enum, a different space), the feature is opt-in and reversible, Swift and Kotlin match, and the reconnect restore sits inside runConnectHandshake, which is WHOOP4-only, so it cannot re-write the 5/MG config flag every connect or collide with the ECG gate's shared 121 read-back.

One thing to change before merge.

The 4.0 path claims a result it did not check

The 5/MG branch right below it writes the flag, reads it back on opcode 121, and its own comment says the write ack is never trusted, only the read-back decides. It then warns that even a confirmed read-back does not prove the strap advertises 0x180D, because some firmware stores the flag and ignores it.

The new path is:

send(.toggleGenericHRProfile, payload: [on ? 0x01 : 0x00])
log("Broadcast HR: WHOOP 4 \(on ? "enable" : "disable") command sent (14).")

You verified 14 on your strap, and I believe it. But this was tested on one 4.0 on one firmware, the toggle reflects a local pref rather than the strap, and a 4.0 that does not implement 14 gives the user a switch that stays on, a log line that reads like success, and no HR broadcast.

Please make the log say what it knows: sent, not confirmed. If command 14 draws a COMMAND_RESPONSE, logging that would be better still.

Smaller

  • testWhoop4EnableFrameMatchesHardwareTestedCommand pins the bytes the code builds, which is worth having, but the hardware evidence is in the PR description and no test can assert what the strap did. The name claims the stronger thing.
  • Where 14 came from is not cited. ABORT_HISTORICAL_TRANSMITS two lines up names its evidence; this one names only the issue.

Before it can be judged green

CI has run nothing: fork workflows are parked awaiting approval, which the API reports as zero checks rather than a failure. The base is also 039dd9392, now eight merges behind, two of which (#2403, #2402) rework WhoopBleClient.kt around this code. Rebase and I will approve the run.

@don86nl
don86nl force-pushed the feat/whoop4-direct-hr-broadcast branch from c0dd5e5 to ec3e80f Compare September 23, 2026 04:32
@don86nl

don86nl commented Sep 23, 2026

Copy link
Copy Markdown
Author

Addressed in ec3e80fd and rebased onto current main (971d0d9f).

  • Both send paths now say the opcode was sent and that the effect is not confirmed.
  • A WHOOP 4 COMMAND_RESPONSE for opcode 14 is logged on Swift and Android with its result/raw frame, explicitly as response evidence rather than a read-back of advertising state.
  • The frame tests are now named MatchesExpectedBytes; the hardware result remains scoped to the PR verification notes.
  • The opcode/name comment now cites the repository's canonical CommandNumber schema and WHOOP 4 command matrix. The boolean payload/effect is separately scoped to the WHOOP 4 tested for this PR.
  • The new UI copy is wired through the existing Apple catalog and Android resources, including the shipped Android locales.

Verification rerun after the rebase:

  • :app:compileFullDebugKotlin
  • :app:testFullDebugUnitTest --tests com.noop.protocol.BroadcastHrConfigTest
  • doc-comment lint
  • protocol source-reference check
  • i18n diff check: no new hardcoded copy or focus-locale gaps

The Swift app target still was not locally compiled because this development host is Windows.

@don86nl
don86nl force-pushed the feat/whoop4-direct-hr-broadcast branch from ec3e80f to 420433a Compare September 23, 2026 04:33
@ryanbr
ryanbr merged commit 4b0cc6b into ryanbr:main Sep 23, 2026
5 checks passed
ryanbr added a commit that referenced this pull request Sep 23, 2026
#2400 flipped "Strap broadcasts its own HR" to a green Yes for the 4.0. That
is the one place the feature asserts the effect works. Every log line on the
same path deliberately refuses to: the strap is asked over
TOGGLE_GENERIC_HR_PROFILE and answers, but nothing reads back whether it then
advertises 0x180D, and the opcode is confirmed on one strap on one firmware.

The comparison screen is where a 4.0 owner decides whether to rely on this, so
it must not claim more than the code does. Partly, in the warning tone, with
the description saying what is and is not known, which is the convention every
other Partly row on this screen already follows.

The 5/MG column keeps Yes: its device-config write is read back on opcode 121.
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