Skip to content

gen4 SET_ALARM_TIME (0x42): the rich form is stored and confirmed but never executed — the official app's rev-1 9-byte form is the one that fires #32

Description

@Pablodvs

Summary

On WHOOP 4.0, the 20-byte 0x04 rich SET_ALARM_TIME body (what cmdSetAlarm builds, and what the comments in lib/src/commands.dart recommend as the firing form) is accepted, stored, echoed back by GET_ALARM, and confirmed with ALARM_SET (event 56) — but the firmware's scheduler never executes it. The band never buzzes and never emits STRAP_DRIVEN_ALARM_EXECUTED (event 57).

The form the official WHOOP app sends, and the only form I have observed actually firing on gen4, is a rev-1 9-byte body:

[0x01] [epoch u32 LE] [subsec u16 LE] [haptic-mode u16 LE = 0]

The trailing haptic-mode u16 is the entire difference from the known-silent 7-byte short form (cmdSetAlarmSimple). So the current comment block is right that the short form silently fails, but wrong about why: it isn't short-vs-rich, it's the missing haptic-mode field. Recommending the rich form ships an alarm that never fires on gen4.

Raw bytes

Official app arming a real 4.0, from a btsnoop capture (noop PR #535), target epoch 1781912880 (0x6A35D530):

01 30 D5 35 6A 00 00 00 00

My own arms of the same layout on my 4.0 (serial 4C1084672), which fired:

01 F4 ED 85 6A 00 00 00 00    → target 1787162100, fired autonomously
01 6C A2 86 6A 00 00 00 00    → target 1787208300, fired autonomously

ALARM_SET (event 56) confirmation frame for the second arm, from the history stream — note the payload echoes the rev-1 body:

30 38 38 00 A6 39 86 6A 38 77 | 08 00 | 01 6C A2 86 6A 00 00 | 10
   seq id=56 ts=1787181478 sub |  ?len | form epoch     subsec | flag?

How I convinced myself

  • Long-baseline negative: 1.07M lines of sync logs covering 8+ alarms armed with the rich form on this band: event 56 confirms every arm, the exact epoch echoes back in the 0x42 response and in GET_ALARM readback, and there is not a single event 57 or any haptics at any armed target. Clock was ruled out on hardware (raw GET_CLOCK readbacks wall-correct to the second; SET_CLOCK latches exactly); band on-wrist at every target.
  • A/B over BLE (2026-08-19): armed the rich form → latches + confirms, no execution, as always. Armed the rev-1 form → at the armed second the band fired autonomously (no phone connected): HAPTICS_FIRED (60), STRAP_DRIVEN_ALARM_EXECUTED (57) and the one-shot auto-disable (59), all stamped at the target epoch, ~24 s buzz, felt on wrist. First event 57 this band has ever emitted.
  • Natural repro next morning (2026-08-20): an app build arming rev-1 fired a real wake alarm at its target (07:45:00), buzz confirmed by HAPTICS_TERMINATED (event 100) stamped exactly target+24 s.

Other empirical facts picked up along the way

  • Alarm lifecycle events (56/57/59/60) are delivered via the history stream on the next sync, not pushed live. An arm can look unconfirmed for up to a sync interval, and the fired events arrive with the sync after the wake.
  • The alarm is one-shot: the band emits ALARM_DISABLED (59) by itself right after executing.
  • HAPTICS_TERMINATED (100) marks the end of the buzz; the stock wake buzz (haptic-mode 0) runs ~24 s.
  • For RESP frames of 0x42–0x45 the armed epoch echoes at inner byte 7.
  • Both forms coexist in the readback path: GET_ALARM faithfully returns whichever body was stored, including bodies the scheduler will never execute — so a readback match is not evidence the alarm will fire.

Suggested changes

  1. Correct the guidance in lib/src/commands.dart (~lines 246–261): on gen4 the rich REVISION_4 body must not be recommended as the firing form — it is stored-but-never-executed.
  2. Add a rev-1 builder ([0x01][epoch u32][subsec u16][haptic-mode u16=0]) as the hardware-verified gen4 firing form.
  3. Keep the gen5 path as-is but flagged: whether the gen5 21-byte slot-1 rich body actually executes is still unverified on hardware (edge #186); the gen4 findings above do not automatically transfer.

Happy to PR 1+2 if the shape sounds right. The corresponding app-side change (edge arming gen4 with rev-1) is a separate PR on edge, fixing edge #119.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions