Skip to content

feat(ble): also scan for the 16-bit FD4B advertisement on 5.0/MG - #1457

Closed
ryanbr wants to merge 1 commit into
mainfrom
feat/whoop5-scan-16bit-uuid
Closed

feat(ble): also scan for the 16-bit FD4B advertisement on 5.0/MG#1457
ryanbr wants to merge 1 commit into
mainfrom
feat/whoop5-scan-16bit-uuid

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Removes a way for 5.0/MG discovery to fail silently. Unconfirmed against hardware — see the honesty
section, which is the part worth reviewing.

The gap

A 128-bit UUID often does not fit the 31-byte advertising payload, so a peripheral may advertise the
16-bit SIG member form instead. Both CoreBluetooth and Android surface that as its Bluetooth-base
expansion 0000FD4B-0000-1000-8000-00805F9B34FB, which does not equal our vendor UUID
fd4b0001-cce1-4033-93ce-002d5875f58a.

Our scans filter on the vendor UUID alone, on both platforms. A strap advertising the 16-bit form would
therefore never appear — and to its owner that reads as "NOOP cannot find my 5.0", not as a discovery
bug worth reporting.

Advertisement-only, deliberately

After connecting, the strap exposes the real 128-bit service in GATT. So retrieveConnectedPeripherals
and discoverServices keep using the vendor UUID alone — widening those would be wrong, not merely
redundant. The new accessor is named for that distinction (advertisedScanServices / advertisedScanUuids)
and a test pins GATT staying single, because the natural mistake when reading this later is to "finish the
job" by widening service discovery too.

Honesty about the evidence

The 16-bit possibility is a fact re-derived from OpenStrap/edge#255 (Dart; no code taken), which ships
the same widening while its own test plan still asks for the nRF Connect capture of primary AD versus
scan response that would settle it. So neither project has confirmed what a real MG advertises.

Straps do pair with NOOP today, so the vendor UUID demonstrably works at least often. What is unknown is
whether it fails for MG specifically, or on particular firmware.

I would normally want the capture first. What makes this shippable without it is that the change is
strictly additive: the vendor UUID stays first in the filter, the 4.0 set is untouched, and there is
no case where a strap that pairs today stops pairing. The bounded downside is slightly more scan traffic
on the 5/MG path.

Verification

  • Mirrored tests both platforms: 4.0 set unchanged, 5/MG set gains exactly the base-expanded form, that
    expansion differs from the vendor UUID, GATT stays single
  • Android 4,089 tests / 0 failures; Kotlin compile + doc-comment gates clean
  • app-build dispatched for the app-target Swift

Still worth asking for

An nRF Connect dump of a real MG (primary AD vs scan response) alongside the rr emit capture #1451 wants
— one volunteer session would close this question for both projects. If it shows the vendor UUID is always
in the primary AD, this can be reverted as unnecessary rather than left as folklore.

A 128-bit UUID often does not fit the 31-byte advertising payload, so a peripheral may advertise the
16-bit SIG member form instead. CoreBluetooth and Android both surface that as its Bluetooth-base
expansion 0000FD4B-0000-1000-8000-00805F9B34FB, which does NOT equal the vendor UUID fd4b0001-... So a
scan filtered only on the vendor UUID would never see such a strap, and the owner would read it as
"NOOP cannot find my 5.0" rather than as a discovery bug.

Advertisement-only, deliberately. After connecting, the strap exposes the real 128-bit service in GATT,
so retrieveConnectedPeripherals and service discovery keep using the vendor UUID alone - widening those
would be wrong, not merely redundant. The new accessor is named for that distinction and the tests pin
it, because the natural mistake when reading this change later is to "finish the job" by widening GATT
too.

UNCONFIRMED against hardware, and worth saying plainly. The 16-bit possibility is a fact re-derived from
OpenStrap/edge#255 (Dart; no code taken), which ships the same widening while its own test plan still
asks for the nRF Connect capture of primary AD versus scan response that would settle it. Straps do pair
with NOOP today, so the vendor UUID demonstrably works at least often; this only removes a way for
discovery to fail silently. The change is strictly additive - the vendor UUID stays first in the filter
and the 4.0 set is untouched - so the downside is bounded to slightly more scan traffic on the 5/MG
path, and there is no case where a strap that pairs today stops pairing.

Mirrored tests on both platforms cover the 4.0 set being unchanged, the 5/MG set gaining exactly the
base-expanded form, that expansion differing from the vendor UUID, and GATT staying single. Android
4,089 tests / 0 failures; Kotlin compile and doc-comment gates clean.
@ryanbr

ryanbr commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

Closing — this cannot work as written, and I should have checked the acceptance path before opening it.

Widening the scan filter only changes which peripherals reach didDiscover. The acceptance decision
is separate and still matches the vendor UUID alone:

if advertised.isEmpty || advertised.contains(selectedServiceUUIDString.lowercased())

with selectedServiceUUIDString: selectedModel.scanService.uuidString — the 128-bit vendor UUID. So a
band advertising only 0000FD4B-… is now surfaced by the wider filter and rejected one line later with
"Discovered … without WHOOP 5.0 / MG service — ignoring". The Add-a-WHOOP wizard sits after that same
check, so it is blocked too.

Net effect: more scan callbacks, zero additional straps found. Pure cost on a path #477 documents as
already tuned — worse than doing nothing.

Making it real would require widening acceptance as well, and that is where it stops being
safe-by-construction: the main scan AUTO-CONNECTS, and 0xFD4B is not confirmed to be WHOOP-exclusive.
Auto-connecting to whatever else advertises a SIG member UUID is a real BLE risk. A wizard-only variant
(where a human confirms the pick) would be defensible, but it is still speculative until a real MG
advertisement is captured, and it is a larger change than this PR claimed to be.

The useful part is the finding, which moves to #1451: NOOP rejects a 16-bit-advertising band at TWO
independent layers, filter and acceptance. Porting OpenStrap/edge#255 shape-for-shape would therefore
produce exactly this inert change — their fix is at the AccessorySetupKit layer, which NOOP does not use.

Revisit only if a capture shows the vendor UUID is absent from the primary AD.

@abdulsaheel

Copy link
Copy Markdown
image

@ryanbr
ryanbr deleted the feat/whoop5-scan-16bit-uuid branch August 23, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants