Explorar o código

Loop error in popup

Ivan Valkou %!s(int64=5) %!d(string=hai) anos
pai
achega
11dd8e6876

+ 59 - 5
FreeAPS/Sources/APS/APSManager.swift

@@ -13,12 +13,34 @@ protocol APSManager {
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var isLooping: CurrentValueSubject<Bool, Never> { get }
-    var lastLoopDate: PassthroughSubject<Date, Never> { get }
+    var lastLoopDate: Date { get }
+    var lastLoopDateSubject: PassthroughSubject<Date, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     func enactTempBasal(rate: Double, duration: TimeInterval)
     func makeProfiles() -> AnyPublisher<Bool, Never>
     func determineBasal() -> AnyPublisher<Bool, Never>
     func roundBolus(amount: Decimal) -> Decimal
+    var lastError: CurrentValueSubject<Error?, Never> { get }
+}
+
+enum APSError: LocalizedError {
+    case pumpError(Error)
+    case invalidPumpState(message: String)
+    case glucoseError(message: String)
+    case apsError(message: String)
+
+    var errorDescription: String? {
+        switch self {
+        case let .pumpError(error):
+            return "Pump error: \(error.localizedDescription)"
+        case let .invalidPumpState(message):
+            return "Invalid Pump State: \(message)"
+        case let .glucoseError(message):
+            return "Invalid glucose: \(message)"
+        case let .apsError(message):
+            return "APS error: \(message)"
+        }
+    }
 }
 
 final class BaseAPSManager: APSManager, Injectable {
@@ -34,6 +56,11 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate: Date = .distantPast
+    @Persisted(key: "lastLoopDate") var lastLoopDate: Date = .distantPast {
+        didSet {
+            lastLoopDateSubject.send(lastLoopDate)
+        }
+    }
 
     private var openAPS: OpenAPS!
 
@@ -45,7 +72,8 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     let isLooping = CurrentValueSubject<Bool, Never>(false)
-    let lastLoopDate = PassthroughSubject<Date, Never>()
+    let lastLoopDateSubject = PassthroughSubject<Date, Never>()
+    let lastError = CurrentValueSubject<Error?, Never>(nil)
 
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> {
         deviceDataManager.pumpDisplayState
@@ -72,11 +100,20 @@ final class BaseAPSManager: APSManager, Injectable {
 
     private func subscribe() {
         deviceDataManager.recommendsLoop
+            .receive(on: processQueue)
             .sink { [weak self] in
                 self?.fetchAndLoop()
             }
             .store(in: &lifetime)
         pumpManager?.addStatusObserver(self, queue: processQueue)
+
+        deviceDataManager.errorSubject
+            .receive(on: processQueue)
+            .map { APSError.pumpError($0) }
+            .sink {
+                self.processError($0)
+            }
+            .store(in: &lifetime)
     }
 
     func heartbeat(date: Date, force: Bool) {
@@ -116,7 +153,7 @@ final class BaseAPSManager: APSManager, Injectable {
                         self.enactSuggested()
                     } else {
                         self.isLooping.send(false)
-                        self.lastLoopDate.send(Date())
+                        self.lastLoopDate = Date()
                     }
                 } else {
                     self.isLooping.send(false)
@@ -127,18 +164,21 @@ final class BaseAPSManager: APSManager, Injectable {
     private func verifyStatus() -> Bool {
         guard let pump = pumpManager else {
             debug(.apsManager, "Pump is not set")
+            processError(APSError.invalidPumpState(message: "Pump is not set"))
             return false
         }
         let status = pump.status.pumpStatus
 
         guard !status.bolusing, !status.suspended else {
             debug(.apsManager, "Pump is bolusing or suspended")
+            processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
             return false
         }
 
         let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
         guard reservoir > 0 else {
             debug(.apsManager, "Reservoir is empty")
+            processError(APSError.invalidPumpState(message: "Reservoir is empty"))
             return false
         }
 
@@ -160,12 +200,14 @@ final class BaseAPSManager: APSManager, Injectable {
     func determineBasal() -> AnyPublisher<Bool, Never> {
         guard let glucose = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
             debug(.apsManager, "Not enough glucose data")
+            processError(APSError.glucoseError(message: "Not enough glucose data"))
             return Just(false).eraseToAnyPublisher()
         }
 
         let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
         guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
             debug(.apsManager, "Glucose data is stale")
+            processError(APSError.glucoseError(message: "Glucose data is stale"))
             return Just(false).eraseToAnyPublisher()
         }
 
@@ -237,6 +279,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 }
             case let .failure(error):
                 debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
+                self.processError(APSError.pumpError(error))
             }
         }
     }
@@ -253,6 +296,7 @@ final class BaseAPSManager: APSManager, Injectable {
                 self.storage.save(temp, as: OpenAPS.Monitor.tempBasal)
             case let .failure(error):
                 debug(.apsManager, "Temp Basal failed with error: \(error.localizedDescription)")
+                self.processError(APSError.pumpError(error))
             }
         }
     }
@@ -366,18 +410,21 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
             isLooping.send(false)
             debug(.apsManager, "Suggestion not found")
+            processError(APSError.apsError(message: "Suggestion not found"))
             return
         }
 
         guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
             isLooping.send(false)
             debug(.apsManager, "Suggestion expired")
+            processError(APSError.apsError(message: "Suggestion expired"))
             return
         }
 
         guard let pump = pumpManager, verifyStatus() else {
             isLooping.send(false)
-            debug(.apsManager, "Invalid pump status")
+            debug(.apsManager, "Invalid pump state")
+            processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
             return
         }
 
@@ -409,13 +456,15 @@ final class BaseAPSManager: APSManager, Injectable {
                 if case let .failure(error) = completion {
                     debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
                     self?.reportEnacted(suggestion: suggested, received: false)
+                    self?.processError(APSError.pumpError(error))
                 } else {
                     self?.reportEnacted(suggestion: suggested, received: true)
                 }
                 self?.isLooping.send(false)
             } receiveValue: {
                 debug(.apsManager, "Loop succeeded")
-                self.lastLoopDate.send(Date())
+                self.lastError.send(nil)
+                self.lastLoopDate = Date()
             }.store(in: &lifetime)
     }
 
@@ -434,6 +483,11 @@ final class BaseAPSManager: APSManager, Injectable {
             nightscout.uploadStatus()
         }
     }
+
+    private func processError(_ error: Error) {
+        debug(.apsManager, "\(error.localizedDescription)")
+        lastError.send(error)
+    }
 }
 
 private extension PumpManager {

+ 8 - 3
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -13,6 +13,7 @@ protocol DeviceDataManager {
     var pumpManager: PumpManagerUI? { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
+    var errorSubject: PassthroughSubject<Error, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
     func heartbeat(date: Date, force: Bool)
@@ -42,6 +43,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         .distantPast
 
     let recommendsLoop = PassthroughSubject<Void, Never>()
+    let errorSubject = PassthroughSubject<Error, Never>()
 
     var pumpManager: PumpManagerUI? {
         didSet {
@@ -210,7 +212,8 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
     func pumpManager(_: PumpManager, didUpdatePumpRecordsBasalProfileStartEvents _: Bool) {}
 
     func pumpManager(_: PumpManager, didError error: PumpManagerError) {
-        debug(.deviceManager, "error: \(error.localizedDescription)")
+        debug(.deviceManager, "error: \(error.localizedDescription), reason: \(String(describing: error.failureReason))")
+        errorSubject.send(error)
         pumpUpdateInProgress = false
     }
 
@@ -297,9 +300,11 @@ extension BaseDeviceDataManager: DeviceManagerDelegate {
         _: DeviceManager,
         logEventForDeviceIdentifier _: String?,
         type _: DeviceLogEntryType,
-        message _: String,
+        message: String,
         completion _: ((Error?) -> Void)?
-    ) {}
+    ) {
+        debug(.deviceManager, message)
+    }
 }
 
 // MARK: - AlertPresenter

+ 14 - 10
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -34,6 +34,8 @@ extension Home {
         @Published var pumpExpiresAtDate: Date?
         @Published var tempTarget: TempTarget?
         @Published var setupPump = false
+        @Published var errorMessage: String? = nil
+        @Published var errorDate: Date? = nil
 
         @Published var allowManualTemp = false
         private(set) var units: GlucoseUnits = .mmolL
@@ -54,16 +56,9 @@ extension Home {
             units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
             closedLoop = settingsManager.settings.closedLoop
-            setStatusTitle()
-
-            if closedLoop,
-               enactedSuggestion?.deliverAt == suggestion?.deliverAt, suggestion?.rate != nil || suggestion?.units != nil
-            {
-                lastLoopDate = enactedSuggestion?.timestamp ?? .distantPast
-            } else {
-                lastLoopDate = suggestion?.timestamp ?? .distantPast
-            }
+            lastLoopDate = apsManager.lastLoopDate
 
+            setStatusTitle()
             setupCurrentTempTarget()
 
             broadcaster.register(GlucoseObserver.self, observer: self)
@@ -91,7 +86,7 @@ extension Home {
                 .assign(to: \.isLooping, on: self)
                 .store(in: &lifetime)
 
-            apsManager.lastLoopDate
+            apsManager.lastLoopDateSubject
                 .receive(on: DispatchQueue.main)
                 .assign(to: \.lastLoopDate, on: self)
                 .store(in: &lifetime)
@@ -105,6 +100,15 @@ extension Home {
                 .receive(on: DispatchQueue.main)
                 .assign(to: \.pumpExpiresAtDate, on: self)
                 .store(in: &lifetime)
+
+            apsManager.lastError
+                .receive(on: DispatchQueue.main)
+                .map { error in
+                    self.errorDate = error == nil ? nil : Date()
+                    return error?.localizedDescription
+                }
+                .assign(to: \.errorMessage, on: self)
+                .store(in: &lifetime)
         }
 
         func addCarbs() {

+ 9 - 1
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -35,7 +35,7 @@ struct LoopView: View {
             if isLooping {
                 Text("looping").font(.caption2)
             } else if actualSuggestion?.timestamp != nil {
-                Text("\(Int((timerDate.timeIntervalSince(lastLoopDate) - Config.lag) / 60) + 1) min ago").font(.caption2)
+                Text(timeString).font(.caption2)
                     .foregroundColor(.secondary)
             } else {
                 Text("--").font(.caption2).foregroundColor(.secondary)
@@ -43,6 +43,14 @@ struct LoopView: View {
         }
     }
 
+    private var timeString: String {
+        let minAgo = Int((timerDate.timeIntervalSince(lastLoopDate) - Config.lag) / 60) + 1
+        if minAgo > 1440 {
+            return "--"
+        }
+        return "\(minAgo) min ago"
+    }
+
     private var color: Color {
         guard actualSuggestion?.timestamp != nil else {
             return .loopGray

+ 13 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -20,6 +20,12 @@ extension Home {
             return formatter
         }
 
+        private var dateFormatter: DateFormatter {
+            let dateFormatter = DateFormatter()
+            dateFormatter.timeStyle = .short
+            return dateFormatter
+        }
+
         var header: some View {
             HStack(alignment: .bottom) {
                 Spacer()
@@ -247,6 +253,13 @@ extension Home {
                     Text(viewModel.statusTitle).foregroundColor(.white)
                         .padding(.bottom, 4)
                     Text(viewModel.suggestion?.reason ?? "No sugestion found").font(.caption).foregroundColor(.white)
+
+                    if let errorMessage = viewModel.errorMessage, let date = viewModel.errorDate {
+                        Text("Error at \(dateFormatter.string(from: date))").foregroundColor(.white)
+                            .padding(.bottom, 4)
+                            .padding(.top, 8)
+                        Text(errorMessage).font(.caption).foregroundColor(.white)
+                    }
                 }
                 .padding()