Skip to content

Haptics: stop the app claiming things the 5/MG buzz cannot do (#926) - #973

Merged
ryanbr merged 2 commits into
mainfrom
fix/haptics-5mg-honesty
Jul 30, 2026
Merged

Haptics: stop the app claiming things the 5/MG buzz cannot do (#926)#973
ryanbr merged 2 commits into
mainfrom
fix/haptics-5mg-honesty

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Items 1 and 2 from #926@vishk23's finding. I verified every claim in that issue against the tree
first; all of them hold, including the two corrections he made to his own earlier statements.

What users see today

On a 5/MG the buzz payload is a hardcoded 12-byte literal whose byte[11] (overallLoop) is 0, so the
caller's repeat count never reaches the wire. All four BuzzPattern choices produce the same buzz — and
the picker offered them ungated on both platforms (NotificationSettingsView.swift:180,
NotificationsSettingsScreen.kt:773).

That is the part worth fixing now: a settings control that does nothing is worse than an absent one,
because the user concludes their strap is broken rather than the feature unsupported.

What this does

Keeps the picker, adds the truth. The choice is stored per app and does apply to a WHOOP 4.0, so
hiding it would lose a real setting. The note follows the honesty wording SmartAlarmView /
SmartAlarmScreen already use for their own 5/MG-unconfirmed case, rather than inventing a pattern.
Translated into all six Android locales and all eight catalogue locales.

Deletes AppModel.buzz(pattern:loops:) — zero callers, and its docstring claimed "used by the
notification-pattern picker"
when the picker calls buzz(loops:).

Corrects two claims that were becoming folklore:

  • BLE_REVERSE_ENGINEERING.md's "7 presets" describes the strap's table, read via
    GET_ALL_HAPTICS_PATTERN (80) — a command neither platform has ever sent, so NOOP has never enumerated
    it. What the app exposes is four, all sharing patternId 2.
  • MaverickHaptics.notificationBuzz now says plainly that nothing ships it, and why it is kept: it is the
    only code that can build a variable-loop body, which is exactly what a future overallLoop != 0 test
    would need.

What this deliberately does NOT do

The payload is untouched. The golden frame is hardware-confirmed at overallLoop = 0, nobody has
tried non-zero on 0x13, and @vishk23 says so himself. The alarm path already ships 7, so the field is
accepted non-zero by the strap — but proving it on the buzz path wants a staged probe (vary one byte, read
back, stop on refusal, as #932 does), not a blanket change to a proven payload. The HELD reasoning in
CHANGELOG.md:2218 still stands.

The live-session intensity levels are also no-ops on 5/MG, but they are internal — no control claims
otherwise — so they get a code note in the follow-up rather than UI.

One deliberate asymmetry in the gate

Android uses stored model || live.whoop5Detected; Apple uses model.whoop5Detected alone. That is not
an oversight:

  • Android's live.whoop5Detected is reset to false on every scan and disconnect
    (WhoopBleClient.kt:2416,2468), so gating on it alone would hide the note whenever the strap is
    offline — which is exactly when someone browses notification settings. Hence the OR, matching the
    Settings iOS: fix restore of WAL-header backups (SQLITE_CANTOPEN) — #18 #22 gate and TestCentreScreen:87.
  • Apple's isWhoop5 reads selectedModel, which persists, so it needs no OR to survive disconnection.

They agree in every settled state. They differ only in the transient first-pair window where a 5/MG is
connected before its pref is written: Android shows the note, Apple doesn't. Closing that would mean
plumbing a live-detection flag onto LiveState for a moment when nobody is on this screen, so it is
left — recorded here rather than discovered later as a divergence.

Verification

i18n_audit --ci and doc_comment_lint clean. The Kotlin edit compiles: a partial compile of the touched
file yields the same two pre-existing errors as main (536 vs 544, the delta being unresolved R.string
and Metrics refs a single-file compile can't see). Android CI and the catalogue jobs are the real gate.

App-target Swift is not compiled by any CI job, so NotificationSettingsView.swift and AppModel.swift
rest on review — the change there is a Text in an existing if and a deletion with no remaining
references, which I checked.

ryanbr added 2 commits July 30, 2026 10:38
From vishk23's finding, whose claims I verified against the tree — all of them
hold.

On a 5/MG the buzz payload is a hardcoded 12-byte literal whose byte[11]
(overallLoop) is 0, so the caller's repeat count never reaches the wire. All
four BuzzPattern choices produce the same buzz, and the picker offered them
ungated on BOTH platforms. A control that silently does nothing is worse than
an absent one: the user concludes the strap is broken rather than the feature
unsupported.

The picker keeps all four — the choice is stored per app and DOES apply to a
WHOOP 4.0, so hiding it would lose a real setting — and now says so, following
the honesty wording SmartAlarmView/SmartAlarmScreen already use for their own
5/MG-unconfirmed case.

Deleted AppModel.buzz(pattern:loops:): zero callers, and its docstring claimed
"used by the notification-pattern picker" when the picker calls buzz(loops:).

Corrected two claims that were becoming folklore:
  - BLE_REVERSE_ENGINEERING's "7 presets" describes the STRAP's own table, read
    via GET_ALL_HAPTICS_PATTERN (80) — a command neither platform has ever sent.
    NOOP has never enumerated it. What the app exposes is four, all patternId 2.
  - MaverickHaptics.notificationBuzz now says plainly that nothing ships it and
    why it is kept: it is the only code that can build a variable-loop body, so
    it is what a future overallLoop != 0 test would need.

The payload itself is deliberately untouched. The golden frame is hardware-
confirmed at 0, nobody has tried non-zero on 0x13, and the alarm path already
ships 7 — so the field is accepted non-zero, but proving it on the buzz wants a
staged probe, not a blanket change to a proven payload.
Re-review catch, and it was a parity bug I introduced.

Android's `live.whoop5Detected` is reset to false on every scan and disconnect
(WhoopBleClient.kt:2416,2468), so gating on it hid the note whenever the strap
was offline — exactly when someone browses notification settings. Apple's
`isWhoop5` reads `selectedModel`, which persists, so the two platforms would
have disagreed about when to show the same sentence.

Now matches the Settings #22 gate that SettingsScreen and TestCentreScreen:87
already use: the stored model OR a live-detected 5/MG this session. The `OR`
keeps the case those two exist for — a 5/MG connected before its pref is
written.
@ryanbr
ryanbr merged commit ad378ce into main Jul 30, 2026
13 checks passed
@ryanbr
ryanbr deleted the fix/haptics-5mg-honesty branch July 30, 2026 18:16
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.

1 participant