Skip to content

Reconnect with a start delay - #103

Merged
ps2 merged 4 commits into
LoopKit:next-devfrom
dnzxy:fix/g7-delayed-reconnect
Oct 31, 2026
Merged

ps2 merged 4 commits into
LoopKit:next-devfrom
dnzxy:fix/g7-delayed-reconnect

Conversation

@dnzxy

@dnzxy dnzxy commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

In direct mode on the phone, readings can stall for 30 to 40 minutes while the app is in the background and only resume once it is opened. This comes from a field report on Trio 1.0.2.35 (dev) and iOS 27.2; the problem predates 27.2. The reporter uses direct mode, and no other BLE device keeps the app awake: their pump has no heartbeat. Cf. nightscout/Trio#1699

What the reporter's log shows

  1. After a disconnect, no reconnect was pending. didDisconnectPeripheral reconnects via scanAfterDelay(), which sleeps 2 s on a global queue. iOS suspends the app before that sleep ends, so bluetoothd has nothing pending and nothing wakes the app:
    09:11:41.682 didDisconnectPeripheral
    (nothing for 31 min)
    09:42:38.723 Retrieved peripheral   <- the frozen delayed scan, firing as the app comes to the foreground
    09:42:38.739 Retrieved peripheral   <- the foreground resumeScanning
    
    The same thing happens at 11:02 → 11:43. That reconnect also lands in the sensor's advertising tail. The disconnect is at 11:01:45, the connect request at 11:01:48, and at 11:02:09 the link comes up and drops in the same millisecond.
  2. The app was suspended between link-up and authentication. didConnect fires at 10:21:43, then nothing is logged until 10:59:25. These links came from the old immediate reconnect landing in the advertising tail, where the link comes up but the sensor doesn't answer, so no Bluetooth event arrives to wake the app. The start delay keeps the reconnect out of the tail.

Change (phone only; no watch changes)

  • When the active peripheral disconnects or fails to connect, connect again immediately with CBConnectPeripheralOptionStartDelayKey set to 30 s. bluetoothd has the request before the app can be suspended, and the delay lets the advertising tail pass. The value has to be a whole number of seconds: CoreBluetooth rejects a fractional one with CBErrorDomain Code=1 ("One or more parameters were invalid"). The request goes through handleDiscoveredPeripheral, so shouldConnectPeripheral still decides. When the link that ended isn't the active peripheral's, or the radio is down, the manager rescans after 2 s as before. That wait is now a managerQueue.asyncAfter, which replaces the Thread.sleep in scanAfterDelay(), and scanAfterDelay() is removed.
  • If a connect fails within 2 s of being requested, it was refused rather than attempted. The manager then waits 30 s and goes back through managerQueue_scanForPeripheral instead of asking again right away. Without this backoff, a request the stack rejects outright turns into a tight connect/fail loop.
  • managerQueue_scanForPeripheral leaves an active peripheral that is .connecting alone. Otherwise the per-cycle fetchNewDataIfNeeded → resumeScanning would replace the delayed request with a plain connect.
  • The reconnect and refusal messages also go to delegate?.bluetoothManager(_:logEvent:), so they appear in the app's own device log and not only in os_log.

Testing

  • Builds against Trio's LoopKit, and the G7SensorKit tests pass.
  • Not yet built for watchOS in a Loop workspace.
  • On device: G7 in direct mode, iPhone on iOS 27.2.
  • Without the phone hold: about 1 h 20 min continuously in the background, 16 of 16 five-minute readings live, each 3 to 6 s after the sensor's reading time, with no refusals or link errors.
  • First revision (which still held the app from connect to disconnect): one continuous run of 13.8 h with the app in the background for all but about ~5 minutes.
    • 155 live readings, 153 connects and 156 delayed reconnect requests. Each reading was requested right after the previous disconnect and arrived 3 to 6 s after the sensor's reading time. Links last about 4 s from connect to disconnect.
    • The refusal backoff never fired. The once-a-minute resumeScanning always found the delayed request still pending and left it alone.
    • 11 of about 166 five-minute slots had no live reading, and the next connect backfilled all of them. These gaps cluster overnight, at -84 to -98 dBm. In all of them the delayed request was still pending, so the sensor simply did not connect. Three gaps include a CBErrorDomain Code=15 encryption timeout, and one a Code=6 link timeout. In each case the next request went out at once, and a later slot connected.
    • Limitation: the test phone's pump (Medtrum) wakes the app every minute, so this run doesn't reproduce the reporter's setup using Omnipod Dash. A run from the reporter or a user using Omnipod Dash would cover that case.

@ps2

ps2 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

The switch to CBConnectPeripheralOptionStartDelayKey, as we discussed, is good. I think it's better than the historic sleep (which might lose a reconnect), but it's a significant change, and probably should get some time on it. Yes, we use it in omnipod, so it seems pretty reliable. But still, this is a really key part of keeping everything working.

As a nit, It looks like maybe scanAfterDelay is no longer used, but wasn't cleaned up.

The part that I'm not sure about is the usage of holdProcess on the phone. It's unclear to me why this is changed.

@dnzxy

dnzxy commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Addressed in 43fc028 and 0d6d608:

  • The phone hold is gone. holdProcess stays in G7WatchAcquisition, unchanged from next-dev; the phone relies on CBConnectPeripheralOptionStartDelayKey alone.
  • scanAfterDelay was still the fallback when the link that ended isn't the active peripheral's, or the radio is off. That path now waits the same 2 s with managerQueue.asyncAfter, and the function is removed.

Agreed on giving the start delay time. Without the hold it has run on my phone for about 1 h 20 min in the background with no gaps (16/16 readings), and before that for about 14 h with the hold. My pump wakes the app every minute, though, so I'll also get a build to the original reporter, whose setup has no other wake source, and post results here.

@dnzxy dnzxy changed the title Reconnect with a start delay; hold through readings Reconnect with a start delay Oct 30, 2026
@ps2

ps2 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Results from about 25 hours on my phone with 0d6d608 (Loop, phone direct mode, iPhone 16 Pro), Oct 30 12:17 to Oct 31 13:33 CDT. The app was in the background most of that time.

The pump's BLE heartbeat was never engaged, and nothing from the pump woke the app, so the G7 alone was keeping Loop running in the background.

Readings

  • 296 of 304 sensor sequence numbers were received live (97.4%).
  • The 8 misses came in 7 gaps: one of 19 min (3 readings) and six of 10 min (1 reading each).
  • Every missed reading was backfilled at the next connection, so the glucose history has no holes.
  • There were no misses in the last 13 hours of the window.

Connectivity

  • There were 296 connect/disconnect cycles. Every one ended with "Disconnected by sensor" (suspectedEndOfSession=false) and reconnected with the 30 s start delay; I didn't see the scanAfterDelay fallback.
  • Every connection reused the saved key ("Already authenticated and bonded"); there were no re-pairings.
  • In each gap, the pending connect simply stayed pending until a later reading, then came up normally. No gap ended because the app was foregrounded: there was no foreground event within 2 minutes before any gap's recovery.
  • Three errors, all recovered from on their own:
    • CBErrorDomain 15 ("Failed to encrypt the connection") at 18:53 during a gap, retried with the start delay;
    • "Error enabling notification for backfill: timeout" at 01:53 and 03:28, with no reading lost either time.
  • Signal ranged from -59 to -98 dBm, averaging -80. Three of the gaps followed readings at -92 to -98 dBm, but two followed readings at -68 and -76.
  • The two state restorations in the window were app relaunches (an install and a restart), and both reconnected.

There were no 30 to 40 minute stalls like the ones in the report.

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