Skip to content

Duplicate device entries for one physical strap — removal doesn't stick (iOS 4.0 + Android 5.0) #1193

Open

Description

@ryanbr

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 fresh whoop-<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 to Whoop5Variant.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's WHOOP 5AG0393796 row is literally the strap's 5AG… serial, sitting beside the
      other row instead of absorbing it.)
    • 4.0: does not expose DIS 0x2A25 at all — strap logs show GET_HELLO_HARVARD but never a
      DIS: serialPrefix=… line. The 4.0 serial lives in the GET_HELLO_HARVARD (cmd 35) response,
      which the interpreter currently decodes for name/firmware but not serial.
  • 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 (repeated Device 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)

  1. Anchor WHOOP identity to the serial (the real cure) — extend the existing
    adoptSerialIdentity path to WHOOP, mirroring adoptOuraSerial, so a re-pair of the same
    physical strap folds onto a stable whoop-<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 transient whoop-<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.0 GET_HELLO_HARVARD response, that unblocks the 4.0 half.
  2. 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

  1. ryanbr commented on Sep 5, 2026

    @ryanbr
    OwnerAuthor

    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.

    • Here, the registry over-splits: one physical strap becomes two rows,
      because the key is a transient CoreBluetooth UUID rather than the serial.
    • In Fusion is keyed by source kind, so a second strap of the same kind has nowhere to live #1895, fusion under-splits: two straps collapse onto one entry, because
      FusionSource is keyed by source KIND and its rawValues are singleton device
      ids ("my-whoop"), so [FusionSource: DailyMetric] has nowhere to put a
      second 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 0x2A25 on 5.0/MG, the cmd-35 GET_HELLO_HARVARD
    response on 4.0, feeding the existing adoptSerialIdentity(from:to:)). Doing the
    fusion 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.

  2. evoveotech commented on Sep 10, 2026

    @evoveotech

    Verified fixed: forgetDevice hard-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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions