Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7bbbe8c
feat(edge): detect + speak WHOOP 5 (gen5) alongside WHOOP 4
abdulsaheel Jul 19, 2026
67fa1cb
fix(edge): centralize gen5 offload command format across all trigger …
abdulsaheel Jul 19, 2026
360ea72
feat(edge): make WHOOP 5 (gen5) pairable — iOS ASK + point protocol a…
abdulsaheel Jul 19, 2026
25de96b
chore(edge): pin pubspec.lock to protocol feat/multiband-whoop5
abdulsaheel Jul 19, 2026
d45d7c3
Merge remote-tracking branch 'origin/main' into feat/multiband-whoop5
abdulsaheel Aug 1, 2026
701b4b4
repin protocol to the merged sha
abdulsaheel Aug 1, 2026
9d13f1b
feat: wire the gen5 session lifecycle through the same BandProfile-ga…
abdulsaheel Aug 1, 2026
aa18782
chore: repin protocol to the real gen5 decoders (412ead9)
abdulsaheel Aug 1, 2026
e21bbaf
repin protocol to c525e29 (cross-validation fixes)
abdulsaheel Aug 1, 2026
260cd0c
bump to 0.9.22+53 for the whoop5 experimental release
abdulsaheel Aug 1, 2026
63a87ed
show which WHOOP generation is connected on the device tile
abdulsaheel Aug 1, 2026
82b094c
bump to 0.9.23+54 for the next whoop5 experimental release
abdulsaheel Aug 1, 2026
7201970
fix(gen5): lenient v18 decode for real WHOOP 5 hardware captures
Brackyt Aug 4, 2026
638c16d
fix(gen5): honest v18 unix + SET_CLOCK revision byte
Brackyt Aug 4, 2026
0ab4e4a
fix(gen5): persist v18 samples to decoded_onehz + align unix gate
Brackyt Aug 4, 2026
1f85b10
fix(db): reject R10-lite before preferred hasDecodedOneHz short-circuit
Brackyt Aug 4, 2026
3a9de5b
test(gen5): add dynamic-gate lenient v18 recovery case
Brackyt Aug 4, 2026
cc53fe1
test(gen5): drop inaccurate dynamic-only lenient case
Brackyt Aug 4, 2026
ab8a386
test(gen5): R10-lite regression uses complete preferred Sample
Brackyt Aug 4, 2026
8e36c3a
fix(db): keep preferred Sample when raw hex is unparseable
Brackyt Aug 5, 2026
9b21e36
fix(gen5): arm wake alarm with official WHOOP slot index 1
Brackyt Aug 5, 2026
584e3a0
fix(gen5): cast Maverick buzz loop clamp back to int
Brackyt Aug 5, 2026
c57e6ff
fix(gen5): arm and decode Maverick live IMU for step calibration
Brackyt Aug 5, 2026
748f6dc
gen5: absent gravity must not read back as perfect stillness
abdulsaheel Aug 6, 2026
52463cc
Merge pull request #188 from Brackyt/fix/whoop5-v18-lenient-decode
abdulsaheel Aug 7, 2026
8638188
Merge pull request #194 from Brackyt/fix/whoop5-gen5-alarm
abdulsaheel Aug 7, 2026
da95af7
test(gen5): use interpolation so `flutter analyze` stays clean
abdulsaheel Aug 7, 2026
048e258
Merge pull request #195 from Brackyt/fix/whoop5-gen5-live-steps
abdulsaheel Aug 7, 2026
9fd17b9
Address CodeRabbit's full-review findings (no merge/conflict work)
abdulsaheel Aug 8, 2026
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
36 changes: 18 additions & 18 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
Expand All @@ -809,9 +809,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -825,9 +825,9 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).RunnerTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
Expand All @@ -849,7 +849,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -865,7 +865,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
Expand Down Expand Up @@ -896,7 +896,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -912,7 +912,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -940,7 +940,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
Expand All @@ -956,7 +956,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -987,7 +987,7 @@
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1003,7 +1003,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
Expand Down Expand Up @@ -1042,7 +1042,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1058,7 +1058,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down Expand Up @@ -1093,7 +1093,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 52;
CURRENT_PROJECT_VERSION = 54;
DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
Expand All @@ -1109,7 +1109,7 @@
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.9.21;
MARKETING_VERSION = 0.9.23;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
51 changes: 30 additions & 21 deletions ios/Runner/AccessorySetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ import AccessorySetupKit
/// - `removeAll` -> nil (deprovision all — used on unpair)
enum AccessorySetup {
private static let channelName = "openstrap/accessory_setup"
// The WHOOP "Harvard" Gen4 GATT service (matches GattUuids.service in Dart).
// `fileprivate` so the iOS-18 Impl below can read it.
fileprivate static let whoopServiceUUID = "61080001-8d6d-82b8-614a-1c8cb0f8dcc6"
// WHOOP GATT service UUIDs, one per generation (match GattProfile in Dart).
// `fileprivate` so the iOS-18 Impl below can read them. BOTH must also be
// listed in Info.plist under NSAccessorySetupBluetoothServices.
// • gen4 ("Harvard", WHOOP 4) — 6108…
// • gen5 ("fd4b", WHOOP 5) — fd4b… (EXPERIMENTAL)
fileprivate static let whoopServiceUUIDGen4 = "61080001-8d6d-82b8-614a-1c8cb0f8dcc6"
fileprivate static let whoopServiceUUIDGen5 = "fd4b0001-cce1-4033-93ce-002d5875f58a"

static func register(messenger: FlutterBinaryMessenger) {
let channel = FlutterMethodChannel(name: channelName, binaryMessenger: messenger)
Expand Down Expand Up @@ -136,30 +140,35 @@ private final class Impl {
return
}

let descriptor = ASDiscoveryDescriptor()
// Match on the WHOOP custom service UUID alone. The foreground scan finds the
// band via startScan(withServices:[thisUUID]) and succeeds, which proves the
// band advertises this service — so it's a reliable, sufficient filter. Every
// descriptor criterion must be declared in Info.plist; the UUID is listed under
// NSAccessorySetupBluetoothServices. (No bluetoothNameSubstring: a single
// descriptor AND-combines its criteria, and a name filter would also require an
// NSAccessorySetupBluetoothNames entry and risk excluding the band on a name
// mismatch.)
descriptor.bluetoothServiceUUID = CBUUID(string: AccessorySetup.whoopServiceUUID)

// Show the actual strap render in the ASK pairing sheet (asset catalog →
// StrapProduct.imageset). Fall back to an SF Symbol if the asset is missing.
// band via startScan(withServices:[…]) and succeeds, which proves the band
// advertises this service — so it's a reliable, sufficient filter. Every
// descriptor criterion must be declared in Info.plist; the UUIDs are listed
// under NSAccessorySetupBluetoothServices. (No bluetoothNameSubstring: a
// single descriptor AND-combines its criteria, and a name filter would also
// require an NSAccessorySetupBluetoothNames entry and risk excluding the band
// on a name mismatch.)
//
// ASK matches ANY item in the picker list, so we offer one item per WHOOP
// generation: gen4 (WHOOP 4) and gen5 (WHOOP 5, experimental). A band that
// advertises either service can be provisioned; the provisioned identifier is
// the same CoreBluetooth UUID regardless of generation.
let productImage = UIImage(named: "StrapProduct")
?? UIImage(systemName: "sensor.tag.radiowave.forward")
?? UIImage()
let item = ASPickerDisplayItem(
name: "WHOOP band",
productImage: productImage,
descriptor: descriptor
)
func item(_ serviceUUID: String, _ name: String) -> ASPickerDisplayItem {
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothServiceUUID = CBUUID(string: serviceUUID)
return ASPickerDisplayItem(
name: name, productImage: productImage, descriptor: descriptor)
}
let items = [
item(AccessorySetup.whoopServiceUUIDGen4, "WHOOP band"),
item(AccessorySetup.whoopServiceUUIDGen5, "WHOOP 5 band"),
]

pickerResult = completion
session.showPicker(for: [item]) { [weak self] error in
session.showPicker(for: items) { [weak self] error in
guard let self = self else { return }
if let error = error {
if let cb = self.pickerResult {
Expand Down
1 change: 1 addition & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<key>NSAccessorySetupBluetoothServices</key>
<array>
<string>61080001-8D6D-82B8-614A-1C8CB0F8DCC6</string>
<string>FD4B0001-CCE1-4033-93CE-002D5875F58A</string>
</array>
<key>NSAccessorySetupKitSupports</key>
<array>
Expand Down
Loading