Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions lib/openstrap_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,6 @@ export 'src/commands.dart'
cmdDisableAlarm,
kDefaultAlarmHaptics,
gen5ClientHello,
cmdGetDataRangeGen5,
cmdSendHistoricalGen5,
cmdSetClockGen5,
cmdGetClockGen5,
cmdBuzzGen5Maverick,
Expand Down
56 changes: 29 additions & 27 deletions lib/src/commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,25 @@ final List<Uint8List> initPackets = [
];

// ── Convenience builders for live ops ──────────────────────────────────────
Uint8List cmdLinkValid(int seq) =>
buildCommand(seq, Cmd.linkValid, const [0x00]);
Uint8List cmdLinkValid(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.linkValid, const [0x00], profile);
Uint8List cmdGetBattery(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.getBatteryLevel, const [], profile);
Uint8List cmdGetHello(int seq) =>
buildCommand(seq, Cmd.getHelloHarvard, const [0x00]);
Uint8List cmdGetHelloModern(int seq) =>
buildCommand(seq, Cmd.getHello, const [0x01]);
Uint8List cmdGetHello(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.getHelloHarvard, const [0x00], profile);
Uint8List cmdGetHelloModern(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.getHello, const [0x01], profile);
Uint8List cmdAbortHistorical(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.abortHistoricalTransmits, const [0x00], profile);
Uint8List cmdSendHistorical(int seq) =>
buildCommand(seq, Cmd.sendHistoricalData, const [0x00]);

/// SEND_HISTORICAL_DATA (0x16) — starts the flash drain.
///
/// gen4 wants a single `[0x00]` byte; gen5 wants an EMPTY body (a non-empty
/// body there was rejected outright, see `cmdGetDataRangeGen5`'s old note —
/// folded into this builder since nothing ever called the gen4 shape on gen5).
Uint8List cmdSendHistorical(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.sendHistoricalData,
profile.isGen5 ? const [] : const [0x00], profile);
/// Read the strap RTC (GET_CLOCK = 0x0B = 11) with an EMPTY body.
///
/// Shared across generations — hardware-verified on WHOOP 5: opcode 11 with
Expand Down Expand Up @@ -153,10 +160,14 @@ Uint8List cmdSetClock(int seq,
return buildCommand(seq, Cmd.setClock, payload, profile);
}

Uint8List cmdGetDataRange(int seq) =>
buildCommand(seq, Cmd.getDataRange, const [0x00]);
Uint8List cmdReportVersionInfo(int seq) =>
buildCommand(seq, Cmd.reportVersionInfo, const []);
/// GET_DATA_RANGE (0x22) — shared opcode, envelope + payload differ by
/// profile: gen4 takes a `[0x00]` body, gen5 expects an EMPTY payload (see
/// control.dart's dual-profile decoder for this opcode).
Uint8List cmdGetDataRange(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.getDataRange,
profile.isGen5 ? const [] : const [0x00], profile);
Uint8List cmdReportVersionInfo(int seq, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.reportVersionInfo, const [], profile);

// Both of these used to send an EMPTY body. gen5 reads the missing first byte
// as revision 0 and rejects the command outright, so neither ever returned
Expand Down Expand Up @@ -226,8 +237,8 @@ Uint8List cmdSelectWrist(int seq, WristSelection selection,
// That puts [cmdEnableOptical] (0x6B) next to the 0x99 persistent-save family
// rather than next to a live stream. Unconfirmed for gen4, so the opcodes are
// left pointed where they are — only the description is corrected.
Uint8List cmdToggleHr(int seq, bool on) =>
buildCommand(seq, Cmd.toggleRealtimeHr, [on ? 0x01 : 0x00]);
Uint8List cmdToggleHr(int seq, bool on, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.toggleRealtimeHr, [on ? 0x01 : 0x00], profile);

/// Toggle the realtime raw (R10/R11) stream (SEND_R10_R11_REALTIME = 0x3F).
///
Expand All @@ -239,8 +250,8 @@ Uint8List cmdToggleHr(int seq, bool on) =>
/// ⚠ That is INVERTED on gen5, which does not implement 0x3F at all: this
/// command is silently ignored there, and 0x51/0x52 ([cmdRawDataStart] /
/// [cmdRawDataStop]) are the realtime-raw start and stop instead.
Uint8List cmdSendR10R11(int seq, bool on) =>
buildCommand(seq, Cmd.sendR10R11Realtime, [on ? 0x01 : 0x00]);
Uint8List cmdSendR10R11(int seq, bool on, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.sendR10R11Realtime, [on ? 0x01 : 0x00], profile);

/// Toggle the IMU data stream (IMU_SET_DATA_STREAM = 0x6A).
///
Expand All @@ -258,8 +269,8 @@ Uint8List cmdToggleImu(int seq, bool on,
: <int>[on ? 0x01 : 0x00],
profile,
);
Uint8List cmdEnableOptical(int seq, bool on) =>
buildCommand(seq, Cmd.enableOpticalData, [revision1, on ? 0x01 : 0x00]);
Uint8List cmdEnableOptical(int seq, bool on, {BandProfile profile = BandProfile.gen4}) =>
buildCommand(seq, Cmd.enableOpticalData, [revision1, on ? 0x01 : 0x00], profile);

/// Play a haptic waveform effect (RUN_HAPTICS_PATTERN = 0x4F).
///
Expand Down Expand Up @@ -602,15 +613,6 @@ Uint8List cmdDisableAlarm(int seq,
Uint8List gen5ClientHello({int seq = 1}) =>
buildCommand(seq, Cmd.getHello, const [0x01], BandProfile.gen5);

/// gen5 GET_DATA_RANGE (0x22) with the EMPTY payload gen5 expects.
Uint8List cmdGetDataRangeGen5(int seq) =>
buildCommand(seq, Cmd.getDataRange, const [], BandProfile.gen5);

/// gen5 SEND_HISTORICAL_DATA (0x16) with the EMPTY payload gen5 expects — the
/// command that starts the flash drain.
Uint8List cmdSendHistoricalGen5(int seq) =>
buildCommand(seq, Cmd.sendHistoricalData, const [], BandProfile.gen5);

// ── gen5 clock (SET_CLOCK_MAVERICK=146 / GET_CLOCK_GEN5=147) ───────────────
//
// gen5 replaces gen4's SET_CLOCK(0x0A)/GET_CLOCK(0x0B) with distinct opcode
Expand Down
34 changes: 33 additions & 1 deletion test/doc_conformance_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,44 @@ void main() {
// zeros — any real body byte here would be a doc deviation.
expect(c.inner.length, 4);
expect(c.inner[3], 0, reason: 'alignment padding, not a body byte');
final r = parseFrame(cmdGetDataRangeGen5(1), profile: BandProfile.gen5)!;
final r = parseFrame(
cmdGetDataRange(1, profile: BandProfile.gen5), profile: BandProfile.gen5)!;
expect(r.inner[2], 34);
expect(r.inner.length, 4);
expect(r.inner[3], 0, reason: 'alignment padding, not a body byte');
});

test('SEND_HISTORICAL_DATA(16) empty body on gen5, [0x00] on gen4', () {
final gen5 = parseFrame(
cmdSendHistorical(1, profile: BandProfile.gen5),
profile: BandProfile.gen5)!;
expect(gen5.inner[2], 22);
expect(gen5.inner.length, 4);
final gen4 = parseFrame(cmdSendHistorical(1))!;
expect(gen4.inner.sublist(2), [22, 0x00]);
});

// These six used to have no [profile] parameter at all, so a caller could
// not ask for gen5 framing — the frame was gen4-shaped (crc8 header)
// regardless, which a real gen5 strap cannot parse. Assert each now frames
// correctly under BOTH profiles, with gen4 output unchanged.
test('previously-unwired builders now frame correctly on gen5', () {
for (final profile in [BandProfile.gen4, BandProfile.gen5]) {
for (final f in <Uint8List>[
cmdLinkValid(1, profile: profile),
cmdGetHello(1, profile: profile),
cmdGetHelloModern(1, profile: profile),
cmdReportVersionInfo(1, profile: profile),
cmdToggleHr(1, true, profile: profile),
cmdSendR10R11(1, true, profile: profile),
cmdEnableOptical(1, true, profile: profile),
]) {
expect(parseFrame(f, profile: profile), isNotNull,
reason: 'must parse under the profile it was built for');
}
}
});

test('toggles — 3 bare bool; 106/107 rev+bool; labrador ops', () {
final hr = parseFrame(cmdToggleHr(1, true), profile: BandProfile.gen4)!;
expect(hr.inner.sublist(2, 4), [3, 0x01], reason: 'opcode 3 takes bare 01');
Expand Down
9 changes: 9 additions & 0 deletions test/whoop_protocol_update_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ void main() {
expect(frame.inner, [0x23, 0x06, 0x22, 0x00]);
});

test('cmdGetDataRange(profile: gen5) frames with the gen5 envelope', () {
final frame = parseFrame(
cmdGetDataRange(0x07, profile: BandProfile.gen5),
profile: BandProfile.gen5)!;
expect(frame.valid, isTrue);
// gen5 body is empty — inner is [type][seq][opcode] padded to /4.
expect(frame.inner, [0x23, 0x07, 0x22, 0x00]);
});

test('cmdSetClock builds the WHOOP-exact 8-byte sec+subsec payload', () {
// Fixed instant: sec = 0x12345678, millis = 500.
// subsec = 500 * 32768 ~/ 1000 = 16384 = 0x4000 (u16 LE, then 2 zero pad).
Expand Down
Loading