Repository navigation
Description
Summary
A single physical WHOOP strap can appear as two entries in Devices, and removing the
duplicate doesn't stick. This is the still-open half of #740 (closed with its WORN / 5-MG parts
resolved, but the reporter explicitly left the duplicate-device part open), now reproduced on a
second platform + strap generation:
| Report | Platform | Strap | The two rows |
|---|---|---|---|
| #740 (Jann1k-h) | Android | WHOOP 5.0 | WHOOP 5.0 Nr.1 (Paired) + WHOOP 5AG0393796 (Active) |
| New (video below) | iOS | WHOOP 4.0 | Theos Whoop (Paired) + Theos WHOOP (Active) — same name, different casing |
Repro video (iOS, 4.0): https://streamable.com/sw53ic — "2 paired", Remove sends one to
REMOVED · Data kept, and it comes back to "2 paired". The friend's strap had been renamed via
the official WHOOP app before pairing in NOOP.
Root cause (identity is not anchored to a stable serial)
NOOP keys a WHOOP device row on a transient identifier, not the strap's serial:
- iOS: a wizard-paired WHOOP gets
id = "whoop-<CBPeripheral.identifier.uuidString>"
(AddDeviceWizard.swift), and the connected uuid comes straight off
peripheral.identifier.uuidString(BLEManager.swift). CoreBluetooth mints a fresh UUID
whenever the OS re-pairs/re-keys the peripheral (which is exactly what "renamed/unbonded via the
official app" triggers) → a freshwhoop-<newUUID>row instead of rejoining the existing one. - The strap's stable serial is available but is never used for identity:
- 5.0 / MG: iOS reads DIS Serial Number String
0x2A25(BLEManager.disSerialChar) but only
feeds it toWhoop5Variant.from(...)for 5.0-vs-MG detection — never to build/merge a device id.
(Duplicate device entries for one physical strap — removal doesn't stick #740'sWHOOP 5AG0393796row is literally the strap's5AG…serial, sitting beside the
other row instead of absorbing it.) - 4.0: does not expose DIS
0x2A25at all — strap logs showGET_HELLO_HARVARDbut never a
DIS: serialPrefix=…line. The 4.0 serial lives in theGET_HELLO_HARVARD(cmd 35) response,
which the interpreter currently decodes for name/firmware but not serial.
- 5.0 / MG: iOS reads DIS Serial Number String
- The existing
adoptSerialIdentity(from:to:)machinery (store +DeviceRegistry, both platforms)
already does exactly the right thing — re-point a transient row onto a stable<prefix>-<serial>
id and merge — but it is wired Oura-only (adoptOuraSerial, gated on the Oura brand prefix).
WHOOP gets no serial merge, so re-pairs pile up as duplicate rows.
Why removal doesn't stick
- Devices "Remove" is a soft archive (status →
archived, data kept); the archived row stays
visible in the REMOVED section forever. There is no hard-delete affordance — an archived
row can only be re-activated ("Make active") or have its data wiped ("Delete this device's
data", which leaves the registry row). So the duplicate can never actually be purged. - On Android, Duplicate device entries for one physical strap — removal doesn't stick #740's log shows the strap is rediscovered and re-targeted within seconds of
removal (repeatedDevice removed — released the strap … (#520)every ~2 min), so even the
archive doesn't hold — the reconnect/scan loop re-targets the same physical strap.
Proposed fix (two parts)
- Anchor WHOOP identity to the serial (the real cure) — extend the existing
adoptSerialIdentitypath to WHOOP, mirroringadoptOuraSerial, so a re-pair of the same
physical strap folds onto a stablewhoop-<serial>id instead of minting a new row. Must
exclude the legacy"my-whoop"id (load-bearing — many consumers hardcode it) and only
re-key transientwhoop-<uuid>rows. Needs the twin on Android for parity.- 5.0 / MG: serial already in hand (DIS
0x2A25). - 4.0: blocked — need to decode the serial out of the
GET_HELLO_HARVARD(cmd 35) response
first, and that offset must be anchored to a raw inbound capture (current strap logs only
record outbound commands + high-level result lines, not the response payload). If someone can
grab a raw 4.0GET_HELLO_HARVARDresponse, that unblocks the 4.0 half.
- 5.0 / MG: serial already in hand (DIS
- Let users clean up — add a hard-delete ("Forget device") for archived rows so an existing
duplicate can be removed, and stop the reconnect/scan loop from re-targeting a strap the user
just removed.
Validation
BLE identity + reconnect behavior can't be CI/Linux-tested — the identity change must be validated
on a real strap that's been through an official-app re-pair (the trigger). The store-level
serial-merge and the hard-delete affordance are unit-testable without hardware.
Refs #740. Reported via Discord + video; friend's WHOOP 4.0 renamed via the official app.
Activity
Android parity: "Forget device" hard-delete for archived rows
Add "Forget device" hard-delete for archived rows (iOS + Android) — #…
Add "Forget device" hard-delete for archived rows (iOS + Android) — r…
Android: fill imported workout HR under the active strap (#1606)
diagnostics: check other device ids before blaming a fresh re-add (#1620
diagnostics: stop calling a second strap's night a read failure (#1667)
feat(ble): adopt a stable whoop-<serial> identity for 5.0/MG (#1303 p…
feat(ble): adopt a stable whoop-<serial> identity for 5.0/MG (#1455)
ryanbr commentedon Sep 5, 2026
Filed #1895 for the same identity problem one layer up, and the two are worth
reading together because the symptoms look opposite while the cause is shared.
because the key is a transient CoreBluetooth UUID rather than the serial.
FusionSourceis keyed by source KIND and its rawValues are singleton deviceids (
"my-whoop"), so[FusionSource: DailyMetric]has nowhere to put asecond strap of the same kind.
Both are the same missing thing - device identity is not a stable first-class
key - and they fail in opposite directions depending on whether the layer
happens to split on something too volatile or something too coarse.
The ordering matters, and it favours this issue. "Key fusion by device" is only
meaningful once a device HAS a durable key, which is exactly the serial
anchoring described above (DIS
0x2A25on 5.0/MG, the cmd-35GET_HELLO_HARVARDresponse on 4.0, feeding the existing
adoptSerialIdentity(from:to:)). Doing thefusion rekey first would just move the guesswork; doing this one first gives the
fusion change something real to key on.
So this is not blocked on #1895, and #1895 should probably wait on this. Noting
it here so whoever picks either one sees the other rather than solving half the
model twice.
ble: give a WHOOP 4.0 the stable serial identity the 5/MG already has (…
evoveotech commentedon Sep 10, 2026
Verified fixed:
forgetDevicehard-delete is implemented on both platforms (Swift:Strand/BLE/BLEManager.swift, Kotlin:android/app/src/main/java/com/noop/ble/WhoopBleClient.kt), removing the device entry entirely so it doesn't reappear.