From 9d598d2ff79d80c287d21842ee6c0c4ffc6f9c7c Mon Sep 17 00:00:00 2001 From: Joe Moran Date: Thu, 7 Mar 2024 23:58:58 -0800 Subject: [PATCH] Eliminate user retries on pod setup resumes (Loop issue #2117 follow up) + Have PairAndPrime ViewModel do an automatic retry on error + Have InsertCannula ViewModel do an automatic retry on error + Add resumingPodSetup func to attempt a getStatus and sleep on errors + Added some improved and updated pumpManager comments + Have pumpManager detect pod setup resumes to invoke resumingPodSetup() + Correct & relocate OmniKit insulinType guard as per OmniBLE + Update OmniKit SessionsRunResult variable use to match OmniBLE --- OmniKit/PumpManager/OmnipodPumpManager.swift | 71 +++++++++++++------ .../ViewModels/InsertCannulaViewModel.swift | 22 +++++- OmniKitUI/ViewModels/PairPodViewModel.swift | 19 ++++- 3 files changed, 87 insertions(+), 25 deletions(-) diff --git a/OmniKit/PumpManager/OmnipodPumpManager.swift b/OmniKit/PumpManager/OmnipodPumpManager.swift index e2ea078..cca749c 100644 --- a/OmniKit/PumpManager/OmnipodPumpManager.swift +++ b/OmniKit/PumpManager/OmnipodPumpManager.swift @@ -749,11 +749,6 @@ extension OmnipodPumpManager { // Called on the main thread public func pairAndPrime(completion: @escaping (PumpManagerResult) -> Void) { - guard let insulinType = insulinType else { - completion(.failure(.configuration(nil))) - return - } - #if targetEnvironment(simulator) // If we're in the simulator, create a mock PodState let mockFaultDuringPairing = false @@ -776,22 +771,22 @@ extension OmnipodPumpManager { let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice let primeSession = { (result: PodComms.SessionRunResult) in switch result { - case .success(let messageSender): + case .success(let session): // We're on the session queue - messageSender.assertOnSessionQueue() + session.assertOnSessionQueue() self.log.default("Beginning pod prime") // Clean up any previously un-stored doses if needed let unstoredDoses = self.state.unstoredDoses - if self.store(doses: unstoredDoses, in: messageSender) { + if self.store(doses: unstoredDoses, in: session) { self.setState({ (state) in state.unstoredDoses.removeAll() }) } do { - let primeFinishedAt = try messageSender.prime() + let primeFinishedAt = try session.prime() completion(.success(primeFinishedAt)) } catch let error { completion(.failure(PumpManagerError.communication(error as? LocalizedError))) @@ -811,9 +806,16 @@ extension OmnipodPumpManager { }) if needsPairing { + guard let insulinType = insulinType else { + completion(.failure(.configuration(OmnipodPumpManagerError.insulinTypeNotConfigured))) + return + } + self.log.default("Pairing pod before priming") - // Create random address with 20 bits to match PDM, could easily use 24 bits instead + // Create random address with 20 bits to match PDM, could easily use 24 bits instead. + // This value is stashed the the OmnipodPumpManagerState as this value cannot vary + // on consecutive Eros pairing attempts to avoid losing the pod in some situations. if self.state.pairingAttemptAddress == nil { self.lockedState.mutate { (state) in state.pairingAttemptAddress = 0x1f000000 | (arc4random() & 0x000fffff) @@ -834,6 +836,7 @@ extension OmnipodPumpManager { } } + // Have new podState, reset all the per pod pump manager state self.resetPerPodPumpManagerState() // Calls completion @@ -842,6 +845,9 @@ extension OmnipodPumpManager { } else { self.log.default("Pod already paired. Continuing.") + // Resuming the pod setup, try to ensure pod comms will work right away + self.resumingPodSetup() + self.podComms.runSession(withName: "Prime pod", using: deviceSelector) { (result) in // Calls completion primeSession(result) @@ -899,16 +905,20 @@ extension OmnipodPumpManager { let timeZone = self.state.timeZone let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice - self.podComms.runSession(withName: "Insert cannula", using: rileyLinkSelector) { (result) in + self.podComms.runSession(withName: "Insert cannula", using: rileyLinkSelector) { (result) in switch result { - case .success(let messageSender): + case .success(let session): + if self.state.podState?.setupProgress.cannulaInsertionSuccessfullyStarted == true { + // Resuming the pod setup, try to ensure pod comms will work right away + self.resumingPodSetup() + } do { if self.state.podState?.setupProgress.needsInitialBasalSchedule == true { let scheduleOffset = timeZone.scheduleOffset(forDate: Date()) - try messageSender.programInitialBasalSchedule(self.state.basalSchedule, scheduleOffset: scheduleOffset) + try session.programInitialBasalSchedule(self.state.basalSchedule, scheduleOffset: scheduleOffset) - messageSender.dosesForStorage() { (doses) -> Bool in - return self.store(doses: doses, in: messageSender) + session.dosesForStorage() { (doses) -> Bool in + return self.store(doses: doses, in: session) } } @@ -920,7 +930,7 @@ extension OmnipodPumpManager { .lowReservoir(self.state.lowReservoirReminderValue) ] - let finishWait = try messageSender.insertCannula(optionalAlerts: alerts) + let finishWait = try session.insertCannula(optionalAlerts: alerts) completion(.success(finishWait)) } catch let error { completion(.failure(.communication(error))) @@ -939,9 +949,9 @@ extension OmnipodPumpManager { let deviceSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Check cannula insertion finished", using: deviceSelector) { (result) in switch result { - case .success(let messageSender): + case .success(let session): do { - try messageSender.checkInsertionCompleted() + try session.checkInsertionCompleted() completion(nil) } catch let error { self.log.error("Failed to fetch pod status: %{public}@", String(describing: error)) @@ -955,6 +965,27 @@ extension OmnipodPumpManager { #endif } + // Called when resuming a pod setup operation which sometimes can fail on the first pod command in various situations. + // Attempting a getStatus and sleeping a couple of seconds on errors greatly improves the odds for first pod command success. + public func resumingPodSetup() { + let sleepTime:UInt32 = 2 + + let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice + podComms.runSession(withName: "Resuming pod setup", using: rileyLinkSelector) { (result) in + switch result { + case .success(let session): + let status = try? session.getStatus() + if status == nil { + self.log.debug("### Pod setup resume getStatus failed, sleeping %d seconds", sleepTime) + sleep(sleepTime) + } + case .failure(let error): + self.log.debug("### Pod setup resume session failure, sleeping %d seconds: %@", sleepTime, error.localizedDescription) + sleep(sleepTime) + } + } + } + // MARK: - Pump Commands public func getPodStatus(completion: ((_ result: PumpManagerResult) -> Void)? = nil) { @@ -1104,9 +1135,9 @@ extension OmnipodPumpManager { let rileyLinkSelector = self.rileyLinkDeviceProvider.firstConnectedDevice self.podComms.runSession(withName: "Deactivate pod", using: rileyLinkSelector) { (result) in switch result { - case .success(let messageSender): + case .success(let session): do { - try messageSender.deactivatePod() + try session.deactivatePod() completion(nil) } catch let error { completion(OmnipodPumpManagerError.communication(error)) diff --git a/OmniKitUI/ViewModels/InsertCannulaViewModel.swift b/OmniKitUI/ViewModels/InsertCannulaViewModel.swift index bb96379..ff80f13 100644 --- a/OmniKitUI/ViewModels/InsertCannulaViewModel.swift +++ b/OmniKitUI/ViewModels/InsertCannulaViewModel.swift @@ -144,16 +144,19 @@ class InsertCannulaViewModel: ObservableObject, Identifiable { var didRequestDeactivation: (() -> Void)? var cannulaInserter: CannulaInserter - + + var autoRetryAttempted: Bool + init(cannulaInserter: CannulaInserter) { self.cannulaInserter = cannulaInserter + self.autoRetryAttempted = false // If resuming, don't wait for the button action if cannulaInserter.cannulaInsertionSuccessfullyStarted { insertCannula() } } - + private func checkCannulaInsertionFinished() { state = .checkingInsertion cannulaInserter.checkCannulaInsertionFinished() { (error) in @@ -169,6 +172,7 @@ class InsertCannulaViewModel: ObservableObject, Identifiable { private func insertCannula() { state = .startingInsertion + cannulaInserter.insertCannula { (result) in DispatchQueue.main.async { switch(result) { @@ -183,7 +187,19 @@ class InsertCannulaViewModel: ObservableObject, Identifiable { self.state = .finished } case .failure(let error): - self.state = .error(error) + if self.autoRetryAttempted { + self.autoRetryAttempted = false // allow for an auto retry on the next user attempt + self.state = .error(error) + } else { + self.autoRetryAttempted = true + let autoRetryPauseTime = TimeInterval(seconds: 3) + print("### insertCannula encountered error \(error.localizedDescription), retrying after \(autoRetryPauseTime) seconds") + DispatchQueue.global(qos: .utility).async { + Thread.sleep(forTimeInterval: autoRetryPauseTime) + + self.insertCannula() + } + } } } } diff --git a/OmniKitUI/ViewModels/PairPodViewModel.swift b/OmniKitUI/ViewModels/PairPodViewModel.swift index ffd79ee..45f5dc1 100644 --- a/OmniKitUI/ViewModels/PairPodViewModel.swift +++ b/OmniKitUI/ViewModels/PairPodViewModel.swift @@ -169,8 +169,11 @@ class PairPodViewModel: ObservableObject, Identifiable { var podPairer: PodPairer + var autoRetryAttempted: Bool + init(podPairer: PodPairer) { self.podPairer = podPairer + self.autoRetryAttempted = false // If resuming, don't wait for the button action if podPairer.podCommState == .activating { @@ -190,8 +193,20 @@ class PairPodViewModel: ObservableObject, Identifiable { DispatchQueue.main.async { switch status { case .failure(let error): - let pairingError = OmnipodPairingError.pumpManagerError(error) - self.state = .error(pairingError) + if self.autoRetryAttempted { + self.autoRetryAttempted = false // allow for an auto retry on the next user attempt + let pairAndPrimeError = OmnipodPairingError.pumpManagerError(error) + self.state = .error(pairAndPrimeError) + } else { + 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) + + self.pairAndPrime() // handles both pairing or priming failures + } + } case .success(let duration): if duration > 0 {