Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# when "Require review from Code Owners" is enabled in branch protection,
# their approval is required before a PR can be merged.

* @marionbarker @itsmojo
* @marionbarker @itsmojo @ps2
68 changes: 38 additions & 30 deletions OmnipodKit/Bluetooth/BlePodComms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,36 +77,44 @@ class BlePodComms: PodComms {
completion(.failure(error))
return
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
let devices = self.bluetoothManager.getConnectedDevices()

if devices.count > 1 {
self.log.default("Multiple pods found while scanning")
self.bluetoothManager.endPodDiscovery()
completion(.failure(PodCommsError.tooManyPodsFound))
timer.invalidate()
}

let elapsed = Date().timeIntervalSince(discoveryStartTime)

// If we've found a pod by 2 seconds, let's go.
if elapsed > TimeInterval(seconds: 2) && devices.count > 0 {
let targetPod = devices.first!
let uuidString = targetPod.manager.peripheral.identifier.uuidString
self.log.default("Found pod UUID %{public}@!", uuidString)
self.bluetoothManager.connectToDevice(uuidString: uuidString)
self.manager = targetPod.manager
targetPod.manager.delegate = self
self.bluetoothManager.endPodDiscovery()
completion(.success(devices.first!))
timer.invalidate()
}

if elapsed > TimeInterval(seconds: 10) {
self.log.default("No pods found while scanning")
self.bluetoothManager.endPodDiscovery()
completion(.failure(PodCommsError.noPodsFound))
timer.invalidate()
// The discoverPods completion can run on a caller thread with no active
// run loop (e.g. a pairing auto-retry from a background queue), where a
// Timer scheduled on the current thread would never fire and discovery
// would hang without ever timing out. Always schedule on the main run loop.
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
let devices = self.bluetoothManager.getConnectedDevices()

if devices.count > 1 {
self.log.default("Multiple pods found while scanning")
self.bluetoothManager.endPodDiscovery()
completion(.failure(PodCommsError.tooManyPodsFound))
timer.invalidate()
return
}

let elapsed = Date().timeIntervalSince(discoveryStartTime)

// If we've found a pod by 2 seconds, let's go.
if elapsed > TimeInterval(seconds: 2) && devices.count > 0 {
let targetPod = devices.first!
let uuidString = targetPod.manager.peripheral.identifier.uuidString
self.log.default("Found pod UUID %{public}@!", uuidString)
self.bluetoothManager.connectToDevice(uuidString: uuidString)
self.manager = targetPod.manager
targetPod.manager.delegate = self
self.bluetoothManager.endPodDiscovery()
completion(.success(targetPod))
timer.invalidate()
return
}

if elapsed > TimeInterval(seconds: 10) {
self.log.default("No pods found while scanning")
self.bluetoothManager.endPodDiscovery()
completion(.failure(PodCommsError.noPodsFound))
timer.invalidate()
}
}
}
}
Expand Down
14 changes: 0 additions & 14 deletions OmnipodKit/PumpManager/OmniPumpManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -438,16 +438,6 @@ public class OmniPumpManager: RileyLinkPumpManager {
backgroundTask.stopBackgroundTask()
}

typealias syncSilencePodStateFuncType = (_ silencePod: Bool, _ silencePodEnd: Date?) -> Void

/// Function to be called when silencePod variables are updated
private var syncSilencePodState: syncSilencePodStateFuncType?

/// Initializes the resetSilencePodState var for callbacks when silence pod mode has changed
func setSyncSilencePodState(_ callbackFunc: @escaping syncSilencePodStateFuncType) {
syncSilencePodState = callbackFunc
}


// MARK: - RileyLink specific vars and funcs

Expand Down Expand Up @@ -704,9 +694,6 @@ extension OmniPumpManager {
doSetSilencePod(session: session, silencePod: false, silencePodEnd: nil) { error in
if let error = error {
self.log.default("@@@ handleSilencePodEnd: disable silence mode failed: %{public}@", error.localizedDescription)
} else {
// Call back to sync the UI silence pod state variables
self.syncSilencePodState?(false, nil)
}
}
}
Expand Down Expand Up @@ -1362,7 +1349,6 @@ extension OmniPumpManager {
blePodComms.connectToNewPod { result in
switch result {
case .failure(let error):
completion(.failure(.communication(error as? LocalizedError)))
completionFailure(error as? LocalizedError)
case .success:
// Have new podState, reset all the per pod pump manager state
Expand Down
34 changes: 13 additions & 21 deletions OmnipodKit/PumpManagerUI/ViewControllers/OmniUICoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ enum OmniUIScreen {
case insulinTypeSelection
case selectPodType
case podTypeSelected // virtual routing step — never presented; resolves to o5KeySetup / rileyLinkSetup / pairAndPrime
case o5KeySetup
case rileyLinkSetup // will be skipped for non-Eros pods
case o5KeySetup // O5 only step
case rileyLinkSetup // Eros only step
case pairAndPrime
case insertCannula
case confirmAttachment
Expand Down Expand Up @@ -90,8 +90,6 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi

weak var completionDelegate: CompletionDelegate?

var podType = unknownOmnipodType

var pumpManager: OmniPumpManager

private var disposables = Set<AnyCancellable>()
Expand Down Expand Up @@ -181,7 +179,6 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi

case .selectPodType:
let didConfirm: (PodType) -> Void = { [weak self] (selectedPodType) in
self?.podType = selectedPodType
self?.pumpManager.podType = selectedPodType
self?.stepFinished()
}
Expand All @@ -190,7 +187,7 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
}

let o5NotAvailable = !isOmnipod5Enabled()
let podTypeSelectionView = PodTypeSelection(initialValue: self.podType, o5NotAvailable: o5NotAvailable, didConfirm: didConfirm, didCancel: didCancel)
let podTypeSelectionView = PodTypeSelection(initialValue: pumpManager.podType, o5NotAvailable: o5NotAvailable, didConfirm: didConfirm, didCancel: didCancel)
let hostedView = hostingController(rootView: podTypeSelectionView)
hostedView.navigationItem.title = LocalizedString("Pod Type", comment: "Title for Pod Type selection screen")
return hostedView
Expand Down Expand Up @@ -236,7 +233,7 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
// We no longer have the certificate for our controllerId.
// Navagiate to select pod type to allow user to select a non O5 pod type
// or to confirm/select O5 pod type and then go through the O5 Setup process again.
self?.podType = unknownOmnipodType // forces user to manually select the pod type
self?.pumpManager.podType = unknownOmnipodType // forces user to manually select the pod type
self?.navigateTo(.selectPodType)
} else {
self?.stepFinished()
Expand All @@ -253,10 +250,6 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
case .settings:
let viewModel = OmniSettingsViewModel(pumpManager: pumpManager)
viewModel.didFinish = { [weak self] in
if self?.pumpManager.podType == unknownOmnipodType {
print("Resetting OmniUICoordinator podType to unknownOmnipodType")
self?.podType = unknownOmnipodType
}
self?.stepFinished()
}
viewModel.navigateTo = { [weak self] (screen) in
Expand Down Expand Up @@ -299,7 +292,7 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
}

let view = hostingController(rootView: PairPodView(viewModel: viewModel).onAppear(perform: {UIApplication.shared.isIdleTimerDisabled = true}), onDisappear: {UIApplication.shared.isIdleTimerDisabled = false})
view.navigationItem.title = String(format: LocalizedString("Pair %1$@ Pod", comment: "Title for pod pairing screen (1: pod type brief name)"), self.podType.briefName)
view.navigationItem.title = String(format: LocalizedString("Pair %1$@ Pod", comment: "Title for pod pairing screen (1: pod type brief name)"), pumpManager.podType.briefName)
view.navigationItem.backButtonDisplayMode = .generic
return view

Expand Down Expand Up @@ -433,13 +426,13 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
// Any caller asking to start pairing — whether through the routing step
// or directly via `.pairAndPrime` (e.g. the Pair Pod button in settings)
// gets diverted to the key setup screen if no cert is loaded.
if podType == omnipod5Type && O5CertificateStore.isEmpty {
if pumpManager.podType == omnipod5Type && O5CertificateStore.isEmpty {
if screen == .podTypeSelected || screen == .pairAndPrime {
return .o5KeySetup
}
}
guard screen == .podTypeSelected else { return screen }
if podType.usesRileyLink {
if pumpManager.podType.usesRileyLink {
return .rileyLinkSetup
}
return .pairAndPrime
Expand Down Expand Up @@ -473,11 +466,11 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
isOnboarded: false,
podState: nil,
timeZone: basalSchedule.timeZone,
basalSchedule: BasalSchedule(repeatingScheduleValues: basalSchedule.items, podType: self.podType),
basalSchedule: BasalSchedule(repeatingScheduleValues: basalSchedule.items, podType: unknownOmnipodType),
maxBasalRateUnitsPerHour: pumpManagerSettings.maxBasalRateUnitsPerHour,
maxBolusUnits: pumpManagerSettings.maxBolusUnits,
insulinType: nil,
podType: self.podType)
podType: unknownOmnipodType)

self.pumpManager = OmniPumpManager(state: pumpManagerState, rileyLinkDeviceProvider: deviceProvider)
} else {
Expand All @@ -501,34 +494,33 @@ class OmniUICoordinator: UINavigationController, PumpManagerOnboarding, Completi
}

private func determineInitialStep() -> OmniUIScreen {
self.podType = pumpManager.podType
if pumpManager.state.podState?.needsCommsRecovery == true {
return .pendingCommandRecovery
} else if pumpManager.podCommState == .activating {
if pumpManager.state.podState?.readyForCannulaInsertion == true && pumpManager.podAttachmentConfirmed {
return .insertCannula
} else {
assert(self.podType != unknownOmnipodType)
assert(pumpManager.podType != unknownOmnipodType)
return .pairAndPrime // need to finish the priming
}
} else if !pumpManager.isOnboarded {
if !pumpManager.initialConfigurationCompleted {
return .firstRunScreen
}
if self.podType == unknownOmnipodType {
if pumpManager.podType == unknownOmnipodType {
return .selectPodType // need to first select a pod type
}
return .podTypeSelected // route to o5KeySetup / rileyLinkSetup / pairAndPrime as appropriate
} else {
if self.podType == unknownOmnipodType {
if pumpManager.podType == unknownOmnipodType {
return .selectPodType // need to first select a pod type
}
// O5 selected, no cert, no active pod: route through pod-type
// reselection so the user can either switch pod type or proceed
// into the O5 cert download (via resolveRoutingStep's
// .podTypeSelected → .o5KeySetup interception) instead of
// landing in Settings where Pair Pod would fail.
if self.podType == omnipod5Type
if pumpManager.podType == omnipod5Type
&& O5CertificateStore.isEmpty
&& pumpManager.podCommState == .noPod {
return .selectPodType
Expand Down
18 changes: 5 additions & 13 deletions OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class OmniSettingsViewModel: ObservableObject {

// Expiration reminder date for current pod
@Published var expirationReminderDate: Date?

var allowedScheduledReminderDates: [Date]? {
return pumpManager.allowedExpirationReminderDates
}
Expand Down Expand Up @@ -254,7 +254,7 @@ class OmniSettingsViewModel: ObservableObject {

init(pumpManager: OmniPumpManager) {
self.pumpManager = pumpManager

lifeState = pumpManager.lifeState
activatedAt = pumpManager.podActivatedAt
expiresAt = pumpManager.expiresAt
Expand All @@ -269,8 +269,8 @@ class OmniSettingsViewModel: ObservableObject {
podCommState = pumpManager.podCommState
beepPreference = pumpManager.beepPreference
silencePodPreference = pumpManager.silencePod ? .enabled : .disabled
podKeepAlivePreference = Storage.shared.podKeepAlive.value
silencePodEnd = pumpManager.silencePodEnd
podKeepAlivePreference = Storage.shared.podKeepAlive.value
hasConnection = pumpManager.hasConnection
insulinType = pumpManager.insulinType
podDetails = pumpManager.podDetails
Expand All @@ -280,8 +280,6 @@ class OmniSettingsViewModel: ObservableObject {
pumpManager.addPodStateObserver(self, queue: DispatchQueue.main)
pumpManager.addStatusObserver(self, queue: DispatchQueue.main)

pumpManager.setSyncSilencePodState(syncSilencePodState)

// Trigger refresh
pumpManager.getPodStatus() { _ in }

Expand Down Expand Up @@ -405,14 +403,6 @@ class OmniSettingsViewModel: ObservableObject {
}
}

/// Called by the Omni pump manager after the silencePodEnd reached and silence pod mode disabled.
func syncSilencePodState(_ silencePod: Bool, _ silencePodEnd: Date?) {
DispatchQueue.main.async {
self.silencePodPreference = silencePod ? .enabled : .disabled
self.silencePodEnd = silencePodEnd
}
}

func setSilencePod(_ silencePodPreference: SilencePodPreference, silencePodEnd: Date?,
_ completion: @escaping (_ error: LocalizedError?) -> Void)
{
Expand Down Expand Up @@ -583,6 +573,8 @@ extension OmniSettingsViewModel: PodStateObserver {
expirationReminderDate = self.pumpManager.scheduledExpirationReminder
podCommState = self.pumpManager.podCommState
beepPreference = self.pumpManager.beepPreference
silencePodPreference = self.pumpManager.silencePod ? .enabled : .disabled
silencePodEnd = self.pumpManager.silencePodEnd
insulinType = self.pumpManager.insulinType
podDetails = self.pumpManager.podDetails
previousPodDetails = self.pumpManager.previousPodDetails
Expand Down
7 changes: 4 additions & 3 deletions OmnipodKit/PumpManagerUI/ViewModels/PairPodViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@ class PairPodViewModel: ObservableObject, Identifiable {
self.autoRetryAttempted = true
let autoRetryPauseTime = TimeInterval(seconds: 3)
print("### pairAndPrimePod encountered error \(error.localizedDescription), retrying after \(autoRetryPauseTime) seconds")
DispatchQueue.global(qos: .utility).async {
Thread.sleep(forTimeInterval: autoRetryPauseTime)

// Retry from the main queue: pairAndPrime() can end up scheduling a
// discovery Timer on the calling thread, which never fires on a
// worker thread with no running run loop.
DispatchQueue.main.asyncAfter(deadline: .now() + autoRetryPauseTime) {
self.pairAndPrime() // handles both pairing or priming failures
}
}
Expand Down