From 5c3bcafb77ca4eb564f99c843a488343d0ff2015 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Sat, 19 Sep 2026 02:19:26 +0530 Subject: [PATCH] gate select_wrist decode on cmd_status like the other confirmation replies a failed select_wrist write still returned a SelectWristResponse built from stale payload bytes, no different from the fixed getBatteryPackInfo/getHello cases. added the status==1 check + a test for the failure case. --- lib/src/control.dart | 15 +++++++++++---- test/whoop_protocol_update_test.dart | 10 ++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/src/control.dart b/lib/src/control.dart index df4b94c..5334635 100644 --- a/lib/src/control.dart +++ b/lib/src/control.dart @@ -1001,10 +1001,17 @@ CmdResponse? parseCommandResponse(Uint8List inner, } else if ((op == Cmd.enterHighFreqSync || op == Cmd.exitHighFreqSync)) { dec['high_freq_sync'] = HighFreqSyncResponse(op); } else if (op == Cmd.selectWrist && payload.length >= 3) { - dec['select_wrist'] = SelectWristResponse( - revision: payload[2], - payload: Uint8List.fromList(payload.sublist(2)), - ); + // Status-gated like getHello above: this is a SET-style confirmation, + // and a failure reply does not populate the body, so its bytes are + // stale. Without the check a rejected wrist-selection write (bad value, + // or refused mid-handshake) would still mint a `select_wrist` object + // that looks like confirmation the selection took effect. + if (status == 1) { + dec['select_wrist'] = SelectWristResponse( + revision: payload[2], + payload: Uint8List.fromList(payload.sublist(2)), + ); + } } else if (op == Cmd.getBatteryPackInfo && payload.length >= 30) { // 28-byte body [rev][attached][id ×6][name ×16][u16][type][status], again // starting at payload[2]. Every field was previously read two bytes early, diff --git a/test/whoop_protocol_update_test.dart b/test/whoop_protocol_update_test.dart index eb3ff13..aafba3e 100644 --- a/test/whoop_protocol_update_test.dart +++ b/test/whoop_protocol_update_test.dart @@ -259,6 +259,16 @@ void main() { expect(ack.revision, 1); expect(ack.payload, [0x01, 0x02]); }); + + test('0x7b select wrist response is dropped when cmd_status is not ok', + () { + // Same stale-but-plausible body as above, but cmd_status = 0 (failed) + // — a failure reply's body is unpopulated, so these bytes must not be + // trusted as confirmation the wrist selection took effect. + final inner = hexToBytes('24037b' '0700' '0102'); + final resp = parseCommandResponse(inner)!; + expect(resp.decoded.containsKey('select_wrist'), isFalse); + }); }); group('WHOOP realtime HR revision 2', () {