Repository navigation
Colmi R11: slow reconnects and misleading Disconnected status #124
Description
Colmi R11: slow reconnects and misleading Disconnected status
A Colmi R11 advertising as M08, using the CRP/Da Rings driver, sometimes needs several attempts to reconnect. Three captured runs on the original build averaged 25 seconds to complete the connection. PulseLoop continues trying, but shows Disconnected between attempts, making recovery look as though it has stopped. On the local fix, all three final manual reconnects finished in under five seconds, with no misleading Disconnected status.
Environment and reproduction
Observed on October 17, 2026, on a Samsung Galaxy A71. The original build was 2.11.0-debug, version code 75. All times below are America/New_York (EDT).
The first captured case occurred when opening the app with a previously connected ring. A second way to exercise the problem was:
- Start with the ring connected.
- Open the ring card from the top-right connection status and select Disconnect.
- Tap "Reconnect last ring" then return to Today and watch the top right indicator.
- Repeat the cycle.
Expected: the app should show Connecting/Reconnecting while a request is active, wait for any previous link teardown, and reach a usable connection without wasting an attempt on that old link.
Actual on the original build: the cold-start case needed three attempts; two later manual reconnects needed three and two attempts. The UI showed Disconnected while automatic recovery was still active. The final table includes all three original cases in the baseline average.
Captured cold-start sequence
| Time (EDT) | Captured event |
|---|---|
| 15:14:40.281 | Opening the app starts its process. |
| 15:14:42.851 | First connection attempt begins. |
| 15:14:47.934 | Android's create-connection timer expires: Connection Accept Timeout, mapped to GATT 133. |
| 15:14:47.970 | PulseLoop records DISCONNECTED. |
| 15:14:57.813 | Automatic recovery starts scanning. |
| 15:14:58.407 | Second connection attempt begins after the ring is found. |
| 15:15:03.436 | Second attempt has the same timeout/GATT 133. |
| 15:15:12.854 | Automatic recovery scans again. |
| 15:15:13.457 | Third connection attempt begins. |
| 15:15:14.522 | Android reports a successful BLE link. |
| 15:15:16.465 | Required notification setup completes; PulseLoop records CONNECTED and subsequently receives history replies. |
This is 33.614 seconds from the first connection attempt, or about 36.2 seconds from process launch. Each failed attempt lasts roughly five seconds, leaving about ten seconds until the next watchdog-driven attempt.
The initial two timeouts occur before service discovery, MTU negotiation, notifications, bonding, or CRP commands. The second attempt follows a fresh scan, so scanning before the first attempt would not explain both failures. The capture does not establish the underlying cause of these GATT 133 timeouts.
What the investigation established
There are three related problems to address.
Recovery waits too long. RingBLEClient.handleDisconnect() originally closed GATT and published DISCONNECTED, leaving recovery to the watchdog's 15-second cadence. The vendor Android app uses the same connectGatt(context, false, callback, TRANSPORT_LE) parameters (decompiled-moyoung-official/sources/d1/d.java), but schedules another attempt after three seconds (o3/l.java, methods F, o, t, and q).
A quick reconnect can race physical teardown. In both original manual cases around 17:09, service discovery returned status=0 count=0. Native logs show Already connected, failed ATT writes to L2CAP, and No server cache available. An empty cache in this sequence is not evidence that the ring lacks its required fdd3 channel. Subsequent tests showed the old physical link disconnecting after a new attempt had already reused it. Closing GATT after the existing 500 ms delay did not establish that physical teardown had finished.
Presentation hides ongoing recovery. The header labeled both disconnected waits and recovery scans as Disconnected, even though the application was still trying.
Local fixes and results
The proposed PR adds a three-second retry for eligible known-CRP disconnects and setup failures, and shows Reconnecting during pending recovery. Setup failures need their own recovery path: failConnectAttempt() clears the active GATT itself, so relying on a later physical-disconnect callback misses that case.
The initial retry/status changes improved the experience but still produced approximately 3, 7, and 10 seconds in three manual runs. The longer two started while the previous physical link was still disconnecting, lost their first attempt, and needed a retry. Those intermediate results are retained in the final table so the remaining failure and the reason for the additional changes are explicit.
The final implementation also waits for both the old GATT close and the matching physical LE disconnect. Android exposes the latter through ACTION_ACL_DISCONNECTED. This observation is shared across foreground and background clients. An already-finished teardown adds no wait; missing confirmation after five seconds requires a fresh advertisement rather than blindly reopening GATT.
The final three reconnects took 3.206, 4.740, and 1.950 seconds, each using one GATT attempt. The first two waited for physical teardown; the third needed no wait. All discovered four services, enabled required notifications, and received vital-history and sleep replies. The tester confirmed Connecting/Reconnecting remained clear until Connected, without a misleading Disconnected state during active recovery.
Validation and limits
The final implementation built successfully and passed 1,547 JVM tests, including retry timing/cancellation, teardown ordering/fallback, and recovery-status coverage. Database integrity and foreign-key checks passed. Existing measurement, activity-day, and sleep-session identities were retained, with unchanged logical contents for all 32 sleep sessions and 647 sleep-stage blocks.
The final manual window contains no stale-link reuse, ATT-to-L2CAP failure, empty service table, GATT 133, or duplicate-client warning. These runs validate the reproduced teardown race, rather than every Bluetooth failure: a separate earlier background attempt still reported GATT 133. The original empty-service failure did not recur after the fixes, so its recovery branch was not independently re-exercised on hardware.
Remaining hardware checks include retry exhaustion while out of range, Disconnect/Forget during pending recovery, Bluetooth/permission changes, foreground/background handoff, and the missed-broadcast fallback. Automatic recovery must continue to respect manual Disconnect and must not open a cancelled connection later.
Related reports reviewed
The issue titles and the relevant discussions below were reviewed before selecting the fix; no matching open report was identified.
- #113 — Disconnected while another internal BLE client holds the ring, closed: Activity recreation created another client while the old one retained GATT ownership. This incident instead shows actual failed attempts and eventual recovery; the cold-start capture has one PulseLoop GATT registration per attempt and no competing-client warning. The process-wide ownership fix must remain intact.
- #72 — Connect does nothing after manual Disconnect, closed: the button called an automatic path that respected the persisted stay-off flag. Here the button does start an attempt. Automatic recovery must still honor that flag until an explicit user reconnect.
- #29 — Colmi R11 pairing, closed: covers generic-name driver selection, bonding, and the eventual CRP driver work. This ring already reaches the CRP driver and syncs after recovery. The observed connection timeouts and teardown ordering do not justify changing driver selection or widening the bonding allowlist.
- #9 — Can't Pair Ring, closed: earlier Colmi pairing background. This report concerns reconnecting a previously working CRP ring and includes native failure-stage evidence.
All captured evaluation runs and averages
Times end at PulseLoop's CONNECTED event after required notification setup. They start at the first logged attempt, or the queued reconnect request where a teardown wait exists; they are not instrumented screen-tap timings. Final totals include the wait. All runs used the same phone/ring, but the original baseline mixes one cold start with two manual reconnects, and the gap after Disconnect varied. These are observed samples, not a controlled benchmark or a guarantee for every future connection.
The separate startup after the initial changes is shown for completeness and excluded from that phase's three-run manual average.
| Build / run | First logged request or attempt (EDT) | CONNECTED (EDT) | Elapsed | GATT attempts | Outcome |
|---|---|---|---|---|---|
| Original — cold start | 15:14:42.851 | 15:15:16.465 | 33.614 s | 3 | Two GATT 133 timeouts before success. |
| Original — manual 1 | 17:09:13.838 | 17:09:39.210 | 25.372 s | 3 | Empty service table on the first attempt; another failed attempt before success. |
| Original — manual 2 | 17:09:53.123 | 17:10:09.124 | 16.001 s | 2 | Empty service table on the first attempt, then success. |
| Initial changes — separate startup | 17:38:05.115 | 17:38:16.403 | 11.288 s | 2 | GATT 133 followed by short-delay recovery; excluded from manual-run average. |
| Initial changes — manual 1 | 17:38:34.921 | 17:38:38.044 | 3.123 s | 1 | Previous physical teardown had already finished. |
| Initial changes — manual 2 | 17:38:43.916 | 17:38:51.010 | 7.094 s | 2 | Reused a link still disconnecting; one delayed retry. |
| Initial changes — manual 3 | 17:38:56.161 | 17:39:06.052 | 9.891 s | 2 | Same teardown race; one delayed retry and longer successful link establishment. |
| Final changes — manual 1 | 19:17:08.273 | 19:17:11.479 | 3.206 s | 1 | Includes 0.320 s waiting for physical teardown. |
| Final changes — manual 2 | 19:17:21.718 | 19:17:26.458 | 4.740 s | 1 | Includes 1.892 s waiting for physical teardown. |
| Final changes — manual 3 | 19:17:32.500 | 19:17:34.450 | 1.950 s | 1 | Physical teardown had finished; no gate wait. |
| Comparison group | Runs included | Average time to CONNECTED |
|---|---|---|
| Previous build | All three original runs: cold start plus two manual reconnects | 25.00 seconds |
| Initial retry/status changes | Three manual reconnects | 6.70 seconds |
| Final proposed PR | Three manual reconnects, including teardown waits | 3.30 seconds |
In these captured runs, average connection time fell from 25.00 seconds to 3.30 seconds—about 13% of the previous time. Results on other CRP rings and phones remain unverified.
PR incoming
A PR will be submitted shortly.
Activity
fix(crp): offer Disconnect while recovery is pending (foureight84#124)
fix(crp): stop teardown recovery for a forgotten ring (foureight84#124)
fix(crp): skip the broadcast wait when the link is already down (four…
fix(crp): resume a teardown fallback scan with the caller's parameters (
fix(crp): read the ACL broadcast device with IntentCompat (foureight8…
fix(crp): coordinate reconnect teardown and recovery (#124)
foureight84 commentedon Oct 20, 2026
@voiceagentscc your PR is in:
📦 PulseLoop 2.11.1 (76) RC1 installs over 2.11.0 (75).
It's your change plus the five follow-up commits described on #125: Disconnect while Reconnecting, Forget stopping a pending reconnect, no 5 s wait when the old link is already down, the fallback scan keeping the caller's ring model, and
IntentCompatfor the ACL broadcast. When you have a moment on the A71, the checks only hardware can answer:The issue closed automatically on merge. Reopen it if anything regresses.