Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
88d32d0
Eager connect watchdog: mitigate InPlay/iPhone-16 connection wedges
ps2 Aug 19, 2026
52db54c
Narrow eager-connect device gate to iPhone 16 family + iPhone 17e only
ps2 Aug 19, 2026
5c08c60
Pod settings: persistent notice for InPlay pod + affected iPhone slow…
ps2 Aug 19, 2026
04f0376
Eager connect tuning from field data: 2s watchdog, 28s budget, dedupe…
ps2 Aug 19, 2026
d993a1a
Pairing: cancel zombie pending connects blocking rediscovered pairabl…
ps2 Aug 19, 2026
66dfba7
Deepen eager-connect budget to 40s (ceiling 45s) for high-wedge-rate …
ps2 Aug 19, 2026
38fd0d6
Consolidate InPlay/affected-iPhone detection into shared definitions
ps2 Aug 20, 2026
45fcca2
Experiment: CBConnectPeripheralOptionEnableAutoReconnect on eager con…
ps2 Aug 20, 2026
84787e9
Remove iPhoneWithPossibleInPlayIssues forwarding var (review feedback)
ps2 Aug 20, 2026
e6b8350
Hold connections longer on eager-gated pods (eagerIdleDisconnectSecon…
ps2 Aug 20, 2026
36a47c6
Raise eager idle-disconnect to 240s to span the inter-cycle gap
ps2 Aug 20, 2026
a3e7bab
Eager idle-disconnect: hold-while-looping (3600s default)
ps2 Aug 20, 2026
41fecf7
Log system auto-reconnect re-establishment latency
ps2 Aug 20, 2026
e523688
Log every iOS-17 didDisconnect(timestamp:isReconnecting:) invocation
ps2 Aug 21, 2026
8a641e5
Foreground: connect aggressively and measure time-to-connect
ps2 Aug 22, 2026
69d1e11
Eager pods: hold link in background via auto-reconnect, eager-connect…
ps2 Aug 23, 2026
0b973ba
Warn when pump-provided heartbeat is requested on a wedge-prone combo
ps2 Aug 23, 2026
d7bfc07
Disconnect-driven heartbeat for wedging setups needing pump wakes
ps2 Aug 23, 2026
0f54368
Merge next-dev (PKA rework #125, pod-fault dose store #119, pair-view…
ps2 Aug 24, 2026
f986a4f
Hold the connection when the app launches straight into the foreground
ps2 Aug 26, 2026
61a2c9c
Correct the test-beeps comment: hasConnection differs by pod type
ps2 Aug 26, 2026
e2f9a5f
Let O5 pods take the fresh-advert connect
ps2 Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions OmnipodKit/Bluetooth/BlePodComms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class BlePodComms: PodComms {

private var bluetoothManager: BluetoothManager!

/// Whether a host has asked the pump to provide the BLE heartbeat (see
/// OmniPumpManager.bleHeartbeatUnsupportedForThisPod).
var isBLEHeartbeatRequested: Bool { bluetoothManager?.isBLEHeartbeatRequested ?? false }

override init(podState: PodState?, podType: PodType, myId: UInt32 = 0, podId: UInt32 = 0) {
super.init(podState: podState, podType: podType, myId: myId, podId: podId)
bluetoothManager = BluetoothManager(podType: podType)
Expand Down
551 changes: 525 additions & 26 deletions OmnipodKit/Bluetooth/BluetoothManager.swift

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions OmnipodKit/Bluetooth/PeripheralManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ extension PeripheralManager {
// disconnect-then-wait stalls). If already connected (burst of sessions), no-op.
if self.peripheral.state != .connected {
do {
try self.connectOnDemand(timeout: 20)
// 45s: sized above BluetoothManager.eagerConnectBudgetSeconds (40s) so the
// eager watchdog's cancel/retry cycles own the recovery underneath this
// single wait, rather than this timeout firing first.
try self.connectOnDemand(timeout: 45)
} catch let error {
self.log.error("[connectOnDemand] on-demand connect failed: %{public}@", String(describing: error))
}
Expand Down Expand Up @@ -661,7 +664,17 @@ extension PeripheralManager {
/// hold the connection separately via `shouldHoldConnection`, so this delay only bites while backgrounded.)
private func scheduleIdleDisconnectIfNeeded() {
guard BluetoothManager.connectOnDemandEnabled else { return }
let idleDelay: TimeInterval = BluetoothManager.idleDisconnectSeconds
// Eager-gated pods (InPlay + affected iPhone): reconnecting costs a wedge storm (median ~10s,
// worst ~30s+), so a working connection is precious. Use a much longer idle window so one loop
// cycle's status→compute→dose burst (sessions ~10-25s apart) shares a single connection instead
// of paying 2-3 storms per cycle. The cycle still ends disconnected — the heartbeat probe
// re-arms ~a minute after the last command, well before the next CGM reading.
let idleDelay: TimeInterval
if bluetoothManager?.shouldUseEagerConnect(for: peripheral) == true {
idleDelay = BluetoothManager.eagerIdleDisconnectSeconds
} else {
idleDelay = BluetoothManager.idleDisconnectSeconds
}
let idleAt = idleStart
queue.asyncAfter(deadline: .now() + idleDelay) { [weak self] in
guard let self = self, BluetoothManager.connectOnDemandEnabled else { return }
Expand Down
51 changes: 51 additions & 0 deletions OmnipodKit/Common/HostAppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// HostAppState.swift
// OmnipodKit
//
// Single seam for host-app lifecycle state. The BLE stack needs to know whether the app is
// frontmost (see BluetoothManager.shouldHoldConnection), but watchOS has no UIApplication —
// keeping the platform split here means BluetoothManager itself stays platform-neutral.
//
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import Foundation
#if os(watchOS)
import WatchKit
#else
import UIKit
#endif

/// Host application lifecycle, abstracted away from UIKit/WatchKit.
///
/// NOTE: the watchOS branch is written against WKApplication (watchOS 9+) but has never been
/// compiled — there is no watch target yet. Verify the symbol names when one lands. On watchOS 8
/// and earlier the equivalents are `WKExtension.shared().applicationState` and
/// `WKExtension.applicationDidBecomeActiveNotification`.
enum HostAppState {

/// True when the host app is frontmost and active. Read this on the main thread.
static var isActive: Bool {
#if os(watchOS)
return WKApplication.shared().applicationState == .active
#else
return UIApplication.shared.applicationState == .active
#endif
}

static var didBecomeActiveNotification: Notification.Name {
#if os(watchOS)
return WKApplication.didBecomeActiveNotification
#else
return UIApplication.didBecomeActiveNotification
#endif
}

static var didEnterBackgroundNotification: Notification.Name {
#if os(watchOS)
return WKApplication.didEnterBackgroundNotification
#else
return UIApplication.didEnterBackgroundNotification
#endif
}
}
10 changes: 10 additions & 0 deletions OmnipodKit/Common/UIDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,15 @@ public extension UIDevice {
return mapToDevice(identifier: identifier)
}()

/// True on the iPhone models known to trigger the InPlay BLE DASH pod firmware bug (the pod
/// silently ignores LL_CONNECTION_PARAM_REQ, wedging connection establishment): the iPhone 16
/// family (all variants, incl. 16e) and the iPhone 17e specifically. Deliberately NOT the rest
/// of the iPhone 17 family, which is unaffected. Single source of truth for all affected-model
/// checks (eager-connect gate, pod keep-alive defaulting, settings advisory).
static var hasPossibleInPlayBLEIssues: Bool {
let model = UIDevice.modelName
return model.contains("iPhone 16") || model == "iPhone 17e"
}

}

19 changes: 9 additions & 10 deletions OmnipodKit/PumpManager/OmniPumpManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1428,7 +1428,7 @@ extension OmniPumpManager {
// Have new podState, reset all the per pod pump manager state
self.resetPerPodPumpManagerState()

if self.usingInPlayPod == true && self.iPhoneWithPossibleInPlayIssues {
if self.usingInPlayPod == true && UIDevice.hasPossibleInPlayBLEIssues {
if self.state.podKeepAlive == .disabled {
// Enable the most conservative pod keep alive mode
// that should continue through the pod setup process.
Expand Down Expand Up @@ -2281,23 +2281,22 @@ extension OmniPumpManager {
}
}

// Running on any iPhone 16 or an iPhone 17e which are known
// to have BLE reconnect issues with InPlay BLE DASH pods?
var iPhoneWithPossibleInPlayIssues: Bool {

let iPhoneModel = UIDevice.modelName
if iPhoneModel.contains("iPhone 16") || iPhoneModel == "iPhone 17e" {
return true
// A host asked the pump to provide the BLE heartbeat on a combination needing the eager-connect
// mitigation. The usual StartDelay probe can't be used there, so wakes are driven by link drops
// instead (see BluetoothManager.isEagerHeartbeatMode) — workable, but less regular.
var bleHeartbeatDegradedForThisPod: Bool {
guard usingInPlayPod == true, UIDevice.hasPossibleInPlayBLEIssues else { return false }
if let blePodComms = podComms as? BlePodComms {
return blePodComms.isBLEHeartbeatRequested
}

return false
}

// Using an InPlay BLE pod?
var usingInPlayPod: Bool? {

if let blePodComms = podComms as? BlePodComms, let deviceBLEName = blePodComms.manager?.peripheral.name {
return deviceBLEName == "InPlay BLE"
return deviceBLEName == BluetoothManager.inPlayPeripheralName
}
return nil // don't know -- maybe not paired yet
}
Expand Down
14 changes: 14 additions & 0 deletions OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ class OmniSettingsViewModel: ObservableObject {
}
}

/// Persistent advisory: this pod uses the InPlay BLE variant AND this iPhone model (iPhone 16
/// family / iPhone 17e) is known to trigger its firmware bug — connections can stall and are
/// automatically retried, so slower-than-normal connects are expected. Shown as a standing
/// notice in settings (with a detail view), not a transient alert.
var connectionSlownessExpected: Bool {
return UIDevice.hasPossibleInPlayBLEIssues && pumpManager.usingInPlayPod == true
}

/// A host asked the pump to provide background heartbeats on a combination needing the eager-connect
/// mitigation: wakes come from link drops rather than the usual timer probe, so they're less regular.
var bleHeartbeatDegraded: Bool {
return pumpManager.bleHeartbeatDegradedForThisPod
}

var isScheduledBasal: Bool {
switch basalDeliveryState {
case .active(_), .initiatingTempBasal:
Expand Down
76 changes: 76 additions & 0 deletions OmnipodKit/PumpManagerUI/Views/InPlayConnectionInfoView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// InPlayConnectionInfoView.swift
// OmnipodKit
//
// Detail screen behind the persistent "slow connections expected" notice shown in pod
// settings when an InPlay-variant DASH pod is paired with an affected iPhone model
// (iPhone 16 family or iPhone 17e). See BluetoothManager's eager-connect watchdog.
//
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import SwiftUI
import UIKit
import LoopKitUI

struct InPlayConnectionInfoView: View {

var body: some View {
List {
Section {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.imageScale(.large)
Text(LocalizedString("Slower Connections Expected", comment: "Title on InPlay connection info view"))
.font(.headline)
}
Text(String(format: LocalizedString("Your pod uses an “InPlay” Bluetooth radio, and your phone (%1$@) is a model known to trigger a bug in that radio’s firmware. When it happens, the Bluetooth connection silently stalls while being established.", comment: "InPlay connection info: what is happening (1: iPhone model name)"), UIDevice.modelName))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 8)
}

Section(header: SectionHeader(label: LocalizedString("What to Expect", comment: "Section header on InPlay connection info view"))) {
VStack(alignment: .leading, spacing: 10) {
Text(LocalizedString("Connecting to the pod may sometimes take noticeably longer than usual — occasionally up to 30 seconds — while stalled attempts are detected and retried automatically. Commands still complete once the connection is made.", comment: "InPlay connection info: what to expect body 1"))
.fixedSize(horizontal: false, vertical: true)
Text(LocalizedString("Pairing a new pod may also need extra time or an additional attempt.", comment: "InPlay connection info: what to expect body 2"))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 4)
}

Section(header: SectionHeader(label: LocalizedString("Pump Heartbeat", comment: "Section header on InPlay connection info view"))) {
VStack(alignment: .leading, spacing: 10) {
Text(LocalizedString("The usual method for the pod to wake the app on a timer can't be used on this combination. Instead, the app is woken when the pod's connection drops, and reconnects right away.", comment: "InPlay connection info: heartbeat body 1"))
.fixedSize(horizontal: false, vertical: true)
Text(LocalizedString("These wake-ups are less regular than usual — roughly every few minutes. If your CGM delivers readings to the app, it provides the wake-ups instead and looping continues normally.", comment: "InPlay connection info: heartbeat body 2"))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 4)
}

Section(header: SectionHeader(label: LocalizedString("What You Can Do", comment: "Section header on InPlay connection info view"))) {
VStack(alignment: .leading, spacing: 10) {
Text(LocalizedString("No action is needed — this is not a pod fault, and insulin delivery is not affected. The pod continues its programmed delivery even while disconnected.", comment: "InPlay connection info: guidance body 1"))
.fixedSize(horizontal: false, vertical: true)
Text(LocalizedString("Keeping your phone near the pod helps connections complete faster. Not every pod uses this radio — a future pod may connect normally.", comment: "InPlay connection info: guidance body 2"))
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 4)
}
}
.insetGroupedListStyle()
.navigationBarTitle(LocalizedString("Pod Connections", comment: "Navigation bar title for InPlay connection info view"), displayMode: .inline)
}
}

struct InPlayConnectionInfoView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
InPlayConnectionInfoView()
}
}
}
52 changes: 51 additions & 1 deletion OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,15 @@ struct OmniSettingsView: View {
.padding(.top,5)
}
.buttonStyle(PlainButtonStyle())
.disabled(!viewModel.hasConnection || sendingTestBeepsCommand)
// Not gated on hasConnection. That var means different things by pod type — the
// pod link for BLE pods, whether ANY RileyLink is connected (independent of pod
// availability) for Eros; see OmniPumpManager.hasConnection. In neither case does
// "not connected right now" mean the command can't run: playTestBeeps goes through
// the normal command path, which acquires the link itself. The icon still greys out
// to show there is no live link, but the button stays tappable, so a beep can be
// used to check whether the pod is actually reachable. Every other action on this
// screen gates on podOk rather than on connectivity.
.disabled(sendingTestBeepsCommand)

headerImage

Expand Down Expand Up @@ -317,6 +325,48 @@ struct OmniSettingsView: View {
}
}

// Persistent advisory for InPlay-variant pods on affected iPhone models (iPhone 16
// family / iPhone 17e): connection establishment can stall and is retried
// automatically, so slower-than-normal connects are expected. Tap for details.
if viewModel.connectionSlownessExpected {
Section {
NavigationLink(destination: InPlayConnectionInfoView()) {
HStack(spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
VStack(alignment: .leading, spacing: 2) {
Text(LocalizedString("Slower Connections Expected", comment: "Title of InPlay connection notice row"))
.font(Font.subheadline.weight(.semibold))
Text(LocalizedString("This pod and phone combination can be slow to connect.", comment: "Subtitle of InPlay connection notice row"))
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
}

// Advisory: a host requested pump-provided background heartbeats. On these combos the
// normal (StartDelay) heartbeat probe can't be used, so wakes come from link drops
// instead — workable, but less regular than on unaffected pods.
if viewModel.bleHeartbeatDegraded {
Section {
NavigationLink(destination: InPlayConnectionInfoView()) {
HStack(spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
VStack(alignment: .leading, spacing: 2) {
Text(LocalizedString("Reduced Background Wake-Ups", comment: "Title of BLE heartbeat degraded notice row"))
.font(Font.subheadline.weight(.semibold))
Text(LocalizedString("Background wake-ups from the pod are less frequent on this pod and phone combination.", comment: "Subtitle of BLE heartbeat degraded notice row"))
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
}

let lifeState = self.viewModel.lifeState
Section(header: SectionHeader(label: LocalizedString("Actions", comment: "Section header for Actions section"))) {
// If need to pair a pod, display this as the only action
Expand Down