feat(ios): discover WHOOP 5.0 / MG in the AccessorySetupKit pairing sheet (experimental) - #238
feat(ios): discover WHOOP 5.0 / MG in the AccessorySetupKit pairing sheet (experimental)#238dev-noaman wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThe PR adds experimental WHOOP Gen 5/MG discovery through iOS descriptors and BLE service matching. It adds unfiltered discovery diagnostics, GATT tree logging, pairing guidance, report display, and shareable log-path access. WHOOP 4 remains the only supported transport family. ChangesWHOOP discovery and diagnostics
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant PairingScreen
participant AppState
participant BleEngine
participant BLEScanner
PairingScreen->>AppState: runDiscoveryProbe()
AppState->>BleEngine: discoveryProbe()
BleEngine->>BLEScanner: perform unfiltered scan
BLEScanner-->>BleEngine: return device and service observations
BleEngine-->>AppState: return report and save diagnostics log
AppState-->>PairingScreen: provide report and log path
PairingScreen-->>PairingScreen: display diagnostics and capture state
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…heet WHOOP 5.0 / MG bands never appear in the ASK pairing sheet. The discovery descriptor and NSAccessorySetupBluetoothServices both declared only the WHOOP 4.0 service UUID (61080001-...), so the sheet had nothing to match and reported "No Accessory Found" even with the band in pairing mode, flashing blue, and visible in system Bluetooth. On iOS 18+ there is no fallback either: the pairing screen returns at the ASK step before the service-filtered scan is reached. Widen discovery without touching the 4.0 path: - Info.plist declares the candidate gen5 service UUID (0xFD4B expanded against the Bluetooth Base UUID) and a "WHOOP" name substring. - The picker is built from three ASPickerDisplayItems -- gen4 by service, gen5 by service, and a name-substring net -- because a single ASDiscoveryDescriptor AND-combines its criteria. - If iOS rejects the widened list, the picker retries once with the 4.0-only item, so the experiment can never break WHOOP 4.0 pairing. - The Dart scan filter (Android / iOS < 18) accepts both families; withServices is OR-combined, so 4.0 discovery is unchanged. - Failed discovery and failed post-connect service discovery now log what was actually seen, and the pairing screen gains an opt-in diagnostics probe that dumps raw advertisements to the shareable log. It appears only after discovery has already failed: running it automatically would trigger the CoreBluetooth permission prompt the ASK flow deliberately avoids. This does NOT make gen5 work. There is no gen5 transport -- a gen5 band that now reaches the connect step still fails at service discovery, by design, logging its real GATT tree. The goal is to turn a silent dead end into a reportable capture. The gen5 service UUID is a candidate from community reports, not from hardware any maintainer owns. It is kept local to the app rather than promoted into openstrap_protocol until a real capture confirms it. Also corrects README, which claimed gen5 bands are "detected and spoken to" -- before this change there was no gen5 code in the tree at all. Testing: flutter analyze clean on all touched Dart; ble_engine_test.dart passes. The iOS native changes are UNBUILT (authored on Windows, no Xcode) and need someone with a Mac to compile and a 4.0 owner to confirm no regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPm2gRRVfrTWeq71Ke6x3Q
b191e69 to
a77e25b
Compare
|
@CodeRabbit review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ios/Runner/AccessorySetup.swift`:
- Around line 156-214: Update makeItem usage in ios/Runner/AccessorySetup.swift:
remove the name-only third descriptor or add a valid bluetoothServiceUUID or
bluetoothCompanyIdentifier alongside bluetoothNameSubstring. Ensure the
corresponding identifier is declared in ios/Runner/Info.plist lines 54-75;
retain the valid Gen4 and Gen5 discovery items and retry behavior.
In `@README.md`:
- Around line 146-152: Update the README checklist statement around the “WHOOP
4.0 only” and “Haven't touched a WHOOP 5” wording to reflect that Gen5/MG
discovery and diagnostics are available. Clarify that WHOOP 4.0 is the only
transport-supported family, while Gen5/MG remains discovery-only and
unvalidated.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: abf60624-eba4-4314-9851-95da8ee53245
📒 Files selected for processing (6)
README.mdios/Runner/AccessorySetup.swiftios/Runner/Info.plistlib/ble/ble_engine.dartlib/state/app_state.dartlib/ui/pairing_screen.dart
| // ONE ITEM PER MATCH STRATEGY. A single ASDiscoveryDescriptor AND-combines its | ||
| // criteria, so a descriptor carrying the 4.0 service AND the gen5 service AND a | ||
| // name substring would match nothing at all. showPicker(for:) takes an array | ||
| // precisely so alternative accessories can each bring their own descriptor; the | ||
| // sheet de-duplicates by peripheral, so a 4.0 band matching two items shows once. | ||
| // | ||
| // Every criterion used below is declared in Info.plist (NSAccessorySetupBluetooth- | ||
| // Services / …Names) — an undeclared criterion is silently ignored by the system, | ||
| // which is exactly how gen5 bands ended up invisible here (no gen5 UUID declared, | ||
| // no name fallback ⇒ "No Accessory Found" no matter what the band was doing). | ||
| func makeItem(_ label: String, | ||
| _ configure: (ASDiscoveryDescriptor) -> Void) -> ASPickerDisplayItem { | ||
| let descriptor = ASDiscoveryDescriptor() | ||
| configure(descriptor) | ||
| return ASPickerDisplayItem(name: label, productImage: productImage, | ||
| descriptor: descriptor) | ||
| } | ||
|
|
||
| let items: [ASPickerDisplayItem] = [ | ||
| // WHOOP 4.0 — the proven path, byte-identical to what shipped before. | ||
| makeItem("WHOOP band") { | ||
| $0.bluetoothServiceUUID = CBUUID(string: AccessorySetup.gen4ServiceUUID) | ||
| }, | ||
| // EXPERIMENTAL — WHOOP 5.0 / MG by its (community-reported) service UUID. | ||
| makeItem("WHOOP 5.0 / MG") { | ||
| $0.bluetoothServiceUUID = CBUUID(string: AccessorySetup.gen5ServiceUUID) | ||
| }, | ||
| // EXPERIMENTAL — WHOOP 5.0 / MG by advertised name, for when the UUID above is | ||
| // wrong or iOS won't surface it. Drop this item once a real capture confirms | ||
| // the gen5 service UUID. | ||
| makeItem("WHOOP band") { | ||
| $0.bluetoothNameSubstring = AccessorySetup.nameSubstring | ||
| }, | ||
| ] | ||
|
|
||
| pickerResult = completion | ||
| session.showPicker(for: [item]) { [weak self] error in | ||
| present(items, allowGen4Retry: true) | ||
| } | ||
|
|
||
| /// Presents the picker and resolves `pickerResult`. | ||
| /// | ||
| /// SAFETY NET for the experimental gen5 items: if the system rejects the descriptor | ||
| /// list outright (e.g. it won't accept a name-only descriptor), we must not take | ||
| /// WHOOP 4.0 pairing down with it — so one retry falls back to the 4.0-only item | ||
| /// that shipped before gen5 support existed. Fail closed on the experiment, never | ||
| /// on the path that works. | ||
| private func present(_ items: [ASPickerDisplayItem], allowGen4Retry: Bool) { | ||
| session.showPicker(for: items) { [weak self] error in | ||
| guard let self = self else { return } | ||
| if let error = error { | ||
| if let cb = self.pickerResult { | ||
| self.pickerResult = nil | ||
| cb(.failure(PickerError(message: error.localizedDescription))) | ||
| // `pickerResult == nil` means .pickerDidDismiss already resolved this as a | ||
| // user cancel, so there is nothing to retry or report. | ||
| guard let cb = self.pickerResult else { return } | ||
| let message = error.localizedDescription | ||
| let looksCancelled = message.lowercased().contains("cancel") | ||
| if allowGen4Retry, !looksCancelled, items.count > 1 { | ||
| NSLog("[ASK] picker rejected the %d-item descriptor list (%@) — " | ||
| + "retrying with the WHOOP 4.0 item only.", items.count, message) | ||
| self.present([items[0]], allowGen4Retry: false) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import plistlib
expected = {
"61080001-8d6d-82b8-614a-1c8cb0f8dcc6",
"0000fd4b-0000-1000-8000-00805f9b34fb",
}
with open("ios/Runner/Info.plist", "rb") as f:
plist = plistlib.load(f)
services = {value.lower() for value in plist["NSAccessorySetupBluetoothServices"]}
names = set(plist["NSAccessorySetupBluetoothNames"])
assert services == expected, services
assert names == {"WHOOP"}, names
print("Info.plist AccessorySetupKit declarations match expected values.")
PY
rg -n -C 2 \
'gen4ServiceUUID|gen5ServiceUUID|nameSubstring|bluetoothServiceUUID|bluetoothNameSubstring|allowGen4Retry' \
ios/Runner/AccessorySetup.swiftRepository: OpenStrap/edge
Length of output: 2744
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- AccessorySetup.swift context ---'
cat -n ios/Runner/AccessorySetup.swift | sed -n '1,240p'
printf '%s\n' '--- iOS project and deployment settings ---'
rg -n -C 2 \
'IPHONEOS_DEPLOYMENT_TARGET|SWIFT_VERSION|SDKROOT|PRODUCT_BUNDLE_IDENTIFIER|NSAccessorySetupBluetooth' \
ios/Runner.xcodeproj ios/Runner ios/Podfile ios/Runner/Info.plist 2>/dev/null || true
printf '%s\n' '--- AccessorySetupKit API usage ---'
rg -n -C 3 \
'ASDiscoveryDescriptor|ASPickerDisplayItem|showPicker|bluetoothServiceUUID|bluetoothNameSubstring|AccessorySetupKit' \
iosRepository: OpenStrap/edge
Length of output: 42335
🌐 Web query:
Apple AccessorySetupKit iOS 18 ASDiscoveryDescriptor bluetoothServiceUUID bluetoothNameSubstring ASPickerDisplayItem showPicker documentation
💡 Result:
AccessorySetupKit, introduced in iOS 18, is a framework designed to provide a privacy-friendly and streamlined process for discovering and configuring Bluetooth and Wi-Fi accessories without requiring broad system-wide permissions [1][2][3]. ASDiscoveryDescriptor The ASDiscoveryDescriptor class defines the criteria used to filter and discover specific accessories [4]. When configuring this descriptor for Bluetooth, you must provide either a bluetoothServiceUUID or a bluetoothCompanyIdentifier [1][4]. Key properties include: - bluetoothServiceUUID: The Bluetooth service UUID associated with the accessory [4]. - bluetoothCompanyIdentifier: The manufacturer ID assigned by the Bluetooth SIG [4]. - bluetoothNameSubstring: An optional property that filters accessories based on their over-the-air Bluetooth name [4]. Note that this property requires at least one of the primary identifiers (service UUID or company ID) to be set, as it identifies the specific service or company associated with that name [4]. ASPickerDisplayItem and showPicker The ASPickerDisplayItem class represents an accessory to be discovered within the system-provided accessory picker [5]. It encapsulates: - A display name and product image for the UI [5]. - An ASDiscoveryDescriptor to define how the system finds the accessory [5]. To trigger the discovery process, you create an instance of ASAccessorySession and call the showPicker(for:completionHandler:) method [1][6]. You pass an array of ASPickerDisplayItem instances to this method, which restricts the picker to showing only those devices that match the provided descriptors [1][6][5]. Implementation Requirements - Info.plist: You must include the NSAccessorySetupKitSupports key in your app's Info.plist, containing the necessary accessory identifiers [1][2][3]. - Migration: If your app previously managed accessories without AccessorySetupKit, you can use ASMigrationDisplayItem (a subclass of ASPickerDisplayItem) to migrate them to the new framework [2][3][6]. - Lifecycle: The picker session operates via the ASAccessorySession, and your app receives event updates through the session's event handler [1][6]. Top results: [1][2][4][6][5]
Citations:
- 1: https://developer.apple.com/documentation/accessorysetupkit/discovering-and-configuring-accessories
- 2: https://developer.apple.com/videos/play/wwdc2024/10203/
- 3: https://www.youtube.com/watch?v=z0YDOmkVMmg
- 4: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor
- 5: https://developer.apple.com/documentation/accessorysetupkit/aspickerdisplayitem
- 6: https://developer.apple.com/documentation/accessorysetupkit/asaccessorysession/showpicker(for:completionhandler:)
🌐 Web query:
site:developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor bluetoothNameSubstring service UUID company identifier required
💡 Result:
In the context of Apple's Accessory Setup Kit, the properties bluetoothNameSubstring [1], bluetoothCompanyIdentifier [2], and bluetoothManufacturerDataMask [3] are optional configuration parameters used within an ASDiscoveryDescriptor to define the criteria for discovering accessories. There is no strict requirement that a bluetoothNameSubstring necessitates a company identifier, nor vice versa. These properties function as individual filters that you can combine as needed to narrow down the discovery process for your specific hardware. An ASDiscoveryDescriptor allows you to specify which Bluetooth advertisements to match, and you may populate only the fields relevant to your device's advertising structure [2][3][1].
Citations:
- 1: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothnamesubstringcompareoptions?changes=lat_2__8_1__5&language=objc
- 2: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothcompanyidentifier?language=objc
- 3: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothmanufacturerdatamask
🌐 Web query:
site:developer.apple.com/documentation/accessorysetupkit/asaccessorysession/showpicker(for:completionhandler:) error completion pickerDidDismiss
💡 Result:
pickerDidDismiss is not an error. It is an ASAccessoryEventType delivered through the session’s event handler when the picker closes. The showPicker completion handler is only for operation errors. (developer.apple.com)
session.activate(on: .main) { event in
switch event.eventType {
case .pickerDidDismiss:
print("Picker dismissed")
case .accessoryAdded:
print("Accessory added")
default:
break
}
}
session.showPicker(for: displayItems) { error in
if let error {
print("Picker error:", error)
}
}If you’re seeing an error after dismissal, log its domain/code; dismissal itself should be handled in the event handler, not interpreted as completionHandler failure. Apple’s sample flow reports accessoryAdded, followed by pickerDidDismiss. (developer.apple.com)
Citations:
- 1: https://developer.apple.com/documentation/accessorysetupkit/asaccessorysession/showpicker%28for%3Acompletionhandler%3A%29?utm_source=openai
- 2: https://developer.apple.com/documentation/accessorysetupkit/discovering-and-configuring-accessories?utm_source=openai
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- Remaining picker implementation ---'
cat -n ios/Runner/AccessorySetup.swift | sed -n '220,320p'
printf '%s\n' '--- Exact AccessorySetupKit declarations ---'
cat -n ios/Runner/Info.plist | sed -n '54,82p'Repository: OpenStrap/edge
Length of output: 3480
🌐 Web query:
Apple AccessorySetupKit ASDiscoveryDescriptor "bluetoothNameSubstring" "at least one"
💡 Result:
In the AccessorySetupKit framework, the bluetoothNameSubstring property of an ASDiscoveryDescriptor is used to specify a substring of an accessory's over-the-air Bluetooth name for discovery [1][2]. It is important to note that a descriptor cannot rely on bluetoothNameSubstring alone. To successfully discover an accessory, the ASDiscoveryDescriptor must include at least one of the following identifying properties: - bluetoothServiceUUID [2][3][4] - bluetoothCompanyIdentifier [2][3][4] If a descriptor is configured with only a bluetoothNameSubstring and lacks both a service UUID and a company identifier, it will be rejected [4]. Once at least one of these primary identifiers is set, you can add the bluetoothNameSubstring as a refinement to narrow the discovery process [3][4]. Additionally, ensure that all identifiers and names used in your descriptors are also declared in the corresponding keys (such as NSAccessorySetupBluetoothServices, NSAccessorySetupBluetoothCompanyIdentifiers, or NSAccessorySetupBluetoothNames) within your app's Info.plist [3][4][5].
Citations:
- 1: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothnamesubstring
- 2: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor
- 3: https://github.com/dpearson2699/swift-ios-skills/blob/main/skills/accessorysetupkit/SKILL.md
- 4: https://github.com/CharlesWiltgen/Axiom/blob/main/.claude-plugin/plugins/axiom/skills/axiom-integration/skills/accessorysetupkit.md
- 5: https://levelup.gitconnected.com/swiftui-pair-ble-accessory-in-an-easy-but-secure-way-a9e88b5e2f07
🌐 Web query:
Apple AccessorySetupKit ASDiscoveryDescriptor "bluetoothServiceUUID" "bluetoothCompanyIdentifier" discovery criteria
💡 Result:
In AccessorySetupKit, the ASDiscoveryDescriptor is used to define the filtering criteria for discovering Bluetooth accessories [1][2]. To successfully configure an ASDiscoveryDescriptor, you must provide at least one of the following two primary identifiers [1][2]: 1. bluetoothServiceUUID: The Bluetooth service UUID associated with the accessory [1][3]. 2. bluetoothCompanyIdentifier: The 16-bit Bluetooth company identifier (manufacturer ID) of the accessory provider [1][4]. Additional filtering criteria can be applied to narrow down discovery, but they often have dependencies on these primary identifiers [1]: - bluetoothNameSubstring: Requires that you also set either a bluetoothServiceUUID or a bluetoothCompanyIdentifier [1]. - bluetoothManufacturerDataBlob and bluetoothManufacturerDataMask: These must be used in conjunction with a bluetoothCompanyIdentifier [1][5]. The manufacturer company ID itself should be excluded from the data blob, as the system handles it via the company identifier property [5]. - bluetoothServiceDataBlob and bluetoothServiceDataMask: These must be used in conjunction with a bluetoothServiceUUID [1]. Both the blob and mask must be of the same length, and they should be less than or equal to the size of the advertised payload [1]. Important Implementation Notes: - Info.plist Configuration: Beyond setting properties in the ASDiscoveryDescriptor, you must also declare the corresponding Bluetooth identifiers in your app's Info.plist using keys such as NSC2AccessorySetupBluetoothServices and NSC2AccessorySetupBluetoothCompanyIdentifiers [5][2]. Failure to include these in your Info.plist may result in the discovery picker failing to find your devices [5]. - Advertising Requirement: For the ASDiscoveryDescriptor to work, the service UUID or company identifier must be actively included in the accessory's Bluetooth advertisement packets; having the identifier only in the GATT table is typically insufficient [6].
Citations:
- 1: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor
- 2: https://github.com/dpearson2699/swift-ios-skills/blob/HEAD/skills/accessorysetupkit/SKILL.md
- 3: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothserviceuuid
- 4: https://developer.apple.com/documentation/accessorysetupkit/asdiscoverydescriptor/bluetoothcompanyidentifier?changes=_6
- 5: https://developer.apple.com/forums/thread/757360
- 6: https://origin-devforums.apple.com/forums/thread/821020
🌐 Web query:
site:developer.apple.com/forums AccessorySetupKit bluetoothNameSubstring ASDiscoveryDescriptor name only
💡 Result:
In AccessorySetupKit, the ASDiscoveryDescriptor object is used to define the criteria for discovering accessories [1][2]. Regarding your question about using bluetoothNameSubstring, developers have reported that relying solely on bluetoothNameSubstring (or a combination of bluetoothNameSubstring and bluetoothServiceUUID) may not trigger successful device discovery in the accessory picker [1][3]. Practical experience from developers indicates that the inclusion of the bluetoothCompanyIdentifier is often necessary for successful BLE discovery, even if it is not explicitly stated as a hard requirement in all documentation [1][3]. When configuring your ASDiscoveryDescriptor, ensure that your Info.plist is also correctly updated with the corresponding keys, such as NSAccessorySetupBluetoothCompanyIdentifiers, to allow the system to scan for the intended manufacturer ID [4]. If you are experiencing issues where the picker remains empty, verify the following: 1. Ensure the bluetoothCompanyIdentifier property is set to your device's correct registered uint16 ID [4]. 2. Confirm that the required Bluetooth-related keys are present in your Info.plist file [1][3]. 3. If your device does not have a unique company identifier, ensure that your discovery descriptor and Info.plist configurations align with the specific requirements for your hardware's advertising format [4]. Because AccessorySetupKit is a modern framework, it is recommended to check the official Apple Developer documentation for ASDiscoveryDescriptor periodically, as implementation requirements and best practices may evolve [1][5].
Citations:
- 1: https://developer.apple.com/forums/tags/accessorysetupkit
- 2: https://developer.apple.com/forums/thread/757333
- 3: https://developer.apple.com/forums/tags/externalaccessory
- 4: https://developer.apple.com/forums/thread/757360
- 5: https://developer.apple.com/forums/thread/817693
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- Local documentation and SDK references ---'
rg -n -i -C 3 \
'bluetoothNameSubstring|bluetoothServiceUUID|ASDiscoveryDescriptor|NSAccessorySetupBluetoothNames|showPicker\(for:' \
. --glob '!ios/Runner/AccessorySetup.swift' --glob '!ios/Runner/Info.plist' \
--glob '!*.lock' 2>/dev/null || trueRepository: OpenStrap/edge
Length of output: 199
Do not pass a name-only ASDiscoveryDescriptor.
bluetoothNameSubstring requires bluetoothServiceUUID or bluetoothCompanyIdentifier. The third item makes the three-item picker request invalid, so the current retry falls back to Gen4 only and Gen5 cannot be discovered. Add a valid primary identifier or remove the name-only item.
📍 Affects 2 files
ios/Runner/AccessorySetup.swift#L156-L214(this comment)ios/Runner/Info.plist#L54-L75
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ios/Runner/AccessorySetup.swift` around lines 156 - 214, Update makeItem
usage in ios/Runner/AccessorySetup.swift: remove the name-only third descriptor
or add a valid bluetoothServiceUUID or bluetoothCompanyIdentifier alongside
bluetoothNameSubstring. Ensure the corresponding identifier is declared in
ios/Runner/Info.plist lines 54-75; retain the valid Gen4 and Gen5 discovery
items and retry behavior.
| - WHOOP 5.0 / MG support is **experimental and discovery-only**. Pairing now looks for | ||
| a gen5 band (by its reported service UUID and by name) instead of silently ignoring | ||
| it, and a Diagnostics button on the pairing screen captures what your phone can | ||
| actually see. But there is no gen5 transport: a 5.0 / MG band that connects will | ||
| still fail at service discovery, on purpose, logging its real GATT tree. Nothing here | ||
| has been validated against 5.0 hardware — no maintainer owns one, so those captures | ||
| are how it gets fixed. WHOOP 4.0 is the only family that actually works. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align the checklist with the Gen5 discovery claim.
Line 146 says Gen5/MG discovery diagnostics exist. README.md Line 82 still says “WHOOP 4.0 only” and “Haven't touched a WHOOP 5.” Users can read these statements as a contradiction. Define WHOOP 4.0 as the only transport-supported family.
Proposed fix
- **WHOOP 4.0 only.** Haven't touched a WHOOP 5, don't know if it even shares a protocol.
+ **WHOOP 4.0 transport only.** WHOOP 5.0/MG discovery diagnostics are experimental. No Gen5 transport is implemented.🧰 Tools
🪛 LanguageTool
[grammar] ~146-~146: Ensure spelling is correct
Context: ...discovery-only**. Pairing now looks for a gen5 band (by its reported service UUID and ...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 146 - 152, Update the README checklist statement
around the “WHOOP 4.0 only” and “Haven't touched a WHOOP 5” wording to reflect
that Gen5/MG discovery and diagnostics are available. Clarify that WHOOP 4.0 is
the only transport-supported family, while Gen5/MG remains discovery-only and
unvalidated.
Fixes the discovery half of #237.
WHOOP 5.0 / MG bands never appear in the AccessorySetupKit pairing sheet: the ASK descriptor and
NSAccessorySetupBluetoothServicesboth declare only the WHOOP 4.0 service UUID, so the sheet has nothing to match and reports "No Accessory Found" even with the band in pairing mode and visible elsewhere in the system. On iOS 18+ there's no fallback either — the pairing screen returns at the ASK step before the service-filtered scan is reached.What this does
Info.plistdeclares the candidate gen5 service UUID (0xFD4Bexpanded against the Bluetooth Base UUID) and aWHOOPname substring. Every descriptor criterion has to be declared here or the system silently ignores it — that declaration gap is the bug.ASPickerDisplayItems — gen4 by service, gen5 by service, and a name-substring net — because a singleASDiscoveryDescriptorAND-combines its criteria, so one descriptor carrying all three would match nothing at all.bluetoothNameSubstring. This retry means the experiment can't take WHOOP 4.0 pairing down with it. Cancellation is distinguished without depending onASErrorsymbol names: a user cancel arrives via.pickerDidDismiss, which already clearspickerResult, so a still-setpickerResultmeans the list itself was rejected.withServicesis OR-combined on both platforms, so 4.0 discovery cannot be narrowed by this.FileLog. It appears only after discovery has already failed — running it automatically would trigger the CoreBluetooth permission prompt that the ASK flow deliberately avoids before provisioning (pairing_screen.dartgates on ASK first precisely because iOS reports the adapter unauthorized until an accessory exists).fd4bappeared in zero files, andsync_policy.dartrecords the one speculative WHOOP5 helper being removed as dead code).What this deliberately does NOT do
It does not make gen5 work. There is no gen5 transport. A gen5 band that now gets provisioned will still fail at service discovery — by design — logging its real GATT tree on the way out. The goal is to turn a silent dead end into a reportable capture, and to unblock discovery for whatever transport comes next.
The gen5 service UUID is a candidate from community reports, not from hardware I own. It's kept local to the app rather than promoted into
openstrap_protocoluntil a real capture confirms it — an unverified guess shouldn't become a protocol constant.Why this is a draft
fd4bturns out to be the head of a custom 128-bit UUID rather than the 16-bit0xFD4B, three constants change — and worse, a 128-bit UUID may land in iOS's hashed overflow area, which would make the name-substring item not a nicety but the only thing that can work on iOS. I'd rather fix that before review than have it merged wrong.I have an MG and can capture whatever you need to settle it (see #237). Happy to rework this however you'd prefer — including throwing out the name-substring approach entirely if you think it's the wrong shape.
Testing
flutter analyze lib/ble/ble_engine.dart lib/ui/pairing_screen.dart lib/state/app_state.dart— clean.flutter test test/ble_engine_test.dart— 10 passed.test/flow_screens_redesign_test.dart(which coversPairingStateView) does not compile on my machine, and did not before this change either:phosphor_flutter 2.1.0hasclass PhosphorIconData extends IconData, andIconDatabecamefinalin Flutter 3.44.6. Unrelated to this PR, but you may want to bump that constraint.PairingStateViewgained only optional parameters, so its existing render tests and every non-failed state are unchanged.Summary by CodeRabbit
New Features
Bug Fixes