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
- 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.
- Add a rev-1 builder (
[0x01][epoch u32][subsec u16][haptic-mode u16=0]) as the hardware-verified gen4 firing form.
- 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.
Summary
On WHOOP 4.0, the 20-byte
0x04rich SET_ALARM_TIME body (whatcmdSetAlarmbuilds, and what the comments inlib/src/commands.dartrecommend 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:
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):My own arms of the same layout on my 4.0 (serial 4C1084672), which fired:
ALARM_SET (event 56) confirmation frame for the second arm, from the history stream — note the payload echoes the rev-1 body:
How I convinced myself
Other empirical facts picked up along the way
Suggested changes
lib/src/commands.dart(~lines 246–261): on gen4 the richREVISION_4body must not be recommended as the firing form — it is stored-but-never-executed.[0x01][epoch u32][subsec u16][haptic-mode u16=0]) as the hardware-verified gen4 firing form.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.