Skip to content

[master] Bug report #1040

Open
Open

Description

@nielsdelange

NOOP version

9.3.0-staging

Platform

Android

OS version

16

WHOOP strap model

WHOOP MG

Where the data is coming from

WHOOP CSV export import

Test Centre profile

Connection & Sync

What happens

My Whoop MG is continuously reconnecting, connected, reconnecting etc.

What you expected instead

A stable fully connected Whoop band

Steps to reproduce

1.close the app, reopen the app
2.disable Bluetooth, enable Bluetooth
3.remove whoop from phone and make new connection

Strap log

<details><summary>Strap log (last 40 lines, redacted)</summary>


11:44:57  sleep day=2026-07-19 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-18 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-17 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-16 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-15 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-14 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-07-13 SKIPPED hrSamples=0 (need ≥200)
11:44:57  sleep day=2026-08-02 totalSleepMin=nil stages=nil eff=nil matched=0 source=computed
11:44:57  hrv day=2026-08-02 window=whole avgHrv=nil
11:44:57  [universal] dayOwner day=2026-08-02 readId=whoop-D0:••:••:••:••:CF writeActiveId=whoop-D0:••:••:••:••:CF hrRows=2439 provenance=measured
11:44:57  fitnessAge gate day=2026-08-02 rhrNights=0 activityDays=1 computed=false
11:44:57  MTU negotiated: 247 (status=0)
11:44:57  Discovering services (mtu=247)
11:44:57  WHOOP 5/MG detected — will send CLIENT_HELLO after subscribing (experimental).
11:44:57  Subscribed 00002a37-0000-1000-8000-00805f9b34fb
11:44:57  Subscribed 00002a19-0000-1000-8000-00805f9b34fb
11:44:57  WHOOP 5/MG: writing CLIENT_HELLO to fd4b0002 with response (to trigger bonding, experimental).
11:44:58  → DISABLE_ALARM payload=02ff (puffin)
11:44:58  Alarm: disarmed (5/MG rev2)
11:44:58  WHOOP 5/MG: live HR streaming — marking the link established (experimental).
11:45:00  Signal: RSSI -49 dBm
11:45:00  [connection] connect down (uptime ends)
11:45:00  [connection] reconnect n=816 reason=localTerminate
11:45:00  Disconnected (status=22); reconnecting directly in 3s (attempt 1, held 3s)
11:45:06  [connection] connect up gen=852 latencyMs=7847081 uptimeStart=1785663906
11:45:06  Connected — requesting MTU 247 before discovery
11:45:07  MTU negotiated: 247 (status=0)
11:45:07  Discovering services (mtu=247)
11:45:07  WHOOP 5/MG detected — will send CLIENT_HELLO after subscribing (experimental).
11:45:07  Subscribed 00002a37-0000-1000-8000-00805f9b34fb
11:45:07  Subscribed 00002a19-0000-1000-8000-00805f9b34fb
11:45:07  WHOOP 5/MG: writing CLIENT_HELLO to fd4b0002 with response (to trigger bonding, experimental).
11:45:08  → DISABLE_ALARM payload=02ff (puffin)
11:45:08  Alarm: disarmed (5/MG rev2)
11:45:08  WHOOP 5/MG: live HR streaming — marking the link established (experimental).
=== Capture check ===
universal: present (dayOwner day=)
connection: MISSING (expected clockDrift )
INCOMPLETE: missing connection
bundle ok files=2 report=present meta=present screenshot=present crash=present leaks=0

</details>

Before you submit

  • I attached my exported NOOP .zip (not a screenshot of the Live screen). If my bug is a clearly reproducible UI issue I described exactly how to see it instead.
  • I attached or pasted a strap log above — or my bug is a clearly reproducible UI issue that doesn't need one (e.g. a typo, a button that does nothing) and I've described exactly how to see it.
  • I checked the Troubleshooting guide — especially "history syncs but no data" (most often the strap isn't banking history to flash; charge it to 100% and reconnect).
  • I checked the FAQ and — if I'm on a WHOOP 5.0 / MG — the 5.0/MG status page, and this isn't an already-known limit (e.g. SpO₂, or recovery/sleep on the 5/MG).
  • I've left out anything personal (strap serial, account email, raw export).

Activity

  1. ryanbr commented on Aug 2, 2026

    @ryanbr
    Owner

    Thanks — this is a good capture, and the log is enough to place it even though it can't yet name the cause.

    What the log says

    The link is being dropped by NOOP, not by the strap:

    [connection] reconnect n=816 reason=localTerminate
    Disconnected (status=22); reconnecting directly in 3s (attempt 1, held 3s)
    

    GATT status 22 is GATT_CONN_TERMINATE_LOCAL_HOST — our own gatt.disconnect(). The strap isn't going away; something on our side is closing the connection about 3 seconds after it comes up, every time, 816 times. The sequence is identical each cycle: MTU 247 → discover → subscribe → CLIENT_HELLO → DISABLE_ALARM → "live HR streaming — marking the link established" → teardown ~3 s later. So the handshake fully succeeds and then something tears it down immediately after.

    That is the same class as #1020 (thousands of localTerminate reconnects).

    What I need to take it further, and why your log can't give it

    There are five different paths in our code that produce this exact status-22 teardown, and your log line is the bare reason=localTerminate — it doesn't say which one fired. That's not your capture's fault: naming the path landed in #1023 after the 9.3.0 tag. On v9.3.0 the code reads

    GATT_CONN_TERMINATE_LOCAL_HOST -> "localTerminate"          // v9.3.0 — no origin

    and on current main it reads

    GATT_CONN_TERMINATE_LOCAL_HOST -> "localTerminate via=${localTeardownOrigin ?: "unknown"}"

    So your 9.3.0-staging build physically cannot print the origin.

    Could you re-capture on the 9.3.1 testing build? https://github.com/ryanbr/noop/releases/tag/testing-latest — NOOP-android-v9.3.1.apk. It installs beside your current app under its own id, so you don't lose anything. It carries #1023 (the via= origin) plus #1027 (offload throughput and interrupted-backfill lines). Same steps — Connection & Sync profile on, let it loop for a minute, export. The via= suffix should point straight at the responsible path, most likely the bond watchdog (#50/#971) given the ~3 s hold, but I don't want to guess when one line will say it.

    Your report was mis-flagged INCOMPLETE — that's our bug, not yours

    Your export ends with:

    connection: MISSING (expected clockDrift )
    INCOMPLETE: missing connection
    

    Ignore that. The Connection domain plainly did capture — your log is full of [connection] connect up / connect down / reconnect n= lines. The completeness check just looks for the wrong marker:

    • Android accepts only clockDrift as proof the Connection mode worked
    • clockDrift is emitted from the strap-clock/history read, which in a reconnect loop never gets a chance to run — so a connection failure guarantees the connection check reads MISSING
    • Android also emits bondState tagged TestDomain.CONNECTION (WhoopBleClient.kt:7578), which would have proved it, but the check doesn't accept that token
    • The Swift twin already accepts both: .connection: ["clockDrift", "bondState"]

    So this is an Android-only gap against the Swift map (whose comment claims the two are byte-identical). Worth fixing so the next connection report isn't dismissed as an incomplete capture.

    Second thing your log exposes

    [connection] connect up gen=852 latencyMs=7847081
    

    7,847,081 ms is 2.18 hours, but the previous drop was 6 seconds earlier. connectAttemptStartedAtMs is assigned only in connect() and never reset on the auto-reconnect path, so after the first attempt the number is measured from the original connect and just grows. In the one scenario this instrumentation exists to diagnose — a reconnect loop — it reports a meaningless figure that could easily be misread as a stall.

    Not a separate problem

    The sleep day=… SKIPPED hrSamples=0 (need ≥200) rows are a consequence, not an extra bug: the link never survives long enough to offload history, so there is nothing to score. They should resolve once the loop is fixed.

  2. ryanbr commented on Aug 2, 2026

    @ryanbr
    Owner

    Correction to my earlier comment: I said the bond watchdog (#50/#971) was the likely culprit "given the ~3 s hold". That is wrong, and it would have sent this down the wrong path.

    BOND_WATCHDOG_MS = 7_000L, and bondWatchdogBackoff only ever WIDENS that window per consecutive bounce. So the bond watchdog cannot fire at 3 seconds — 7 s is its floor. Your log shows the link held for 3 s, every cycle:

    11:44:57  ... link established
    11:45:00  [connection] connect down (uptime ends)
    11:45:00  Disconnected (status=22); ... (attempt 1, held 3s)
    

    That rules the watchdog out rather than implicating it. The keep-alive bounce is a 120 s liveness check, so that is out too. What remains among our own teardown routes is a GATT operation throwing (gattThrow:<op>), or the multi-WHOOP re-adopt — and re-adopt is precisely the one route that was never tagged, so it would print via=unknown rather than naming itself.

    So the re-capture matters more than I implied, and I should not have offered a guess ahead of it.

    One practical note on which build to use. The 9.3.1 testing build tags four of the five routes. If your capture comes back via=unknown, that is not a dead end — with four routes named, unknown points at the untagged re-adopt path by elimination. #1046 tags that fifth route and also stops the Connection capture flagging itself INCOMPLETE, so a build cut after it merges would name all five directly. Either build is worth capturing on; the newer one just answers it in one step instead of two.

    To be explicit about scope: #1046 does not change any connection behaviour. It will not stop your strap reconnecting. It exists so the next capture identifies which code path is dropping the link, which is what a real fix has to be built on.

  3. nielsdelange commented on Aug 2, 2026

    @nielsdelange
    Author

    I did a full hard reset on the Whoop band, did the pairing again and for now it is stable, I will keep you posted!

  4. ryanbr commented on Aug 3, 2026

    @ryanbr
    Owner

    Would test with the latest build, if it happens again we'll have additional debug info

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