Przeglądaj źródła

Release/0.1.14 (#29)

* Pump suspensions on basal chart

* show pump basal profile if no looping

* a manual bolus limited by maxBolus

* Bold glucose delta

* fix bolus calculation

* bump version
Ivan 5 lat temu
rodzic
commit
48180ec86b

+ 2 - 2
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -24,8 +24,8 @@
         "repositoryURL": "https://github.com/apple/swift-algorithms",
         "state": {
           "branch": null,
-          "revision": "1e761dd787b0f148f0b7aec42a7ff401767c26fa",
-          "version": "0.0.3"
+          "revision": "04d91803a2dd58c14db9cf66c7412c6dd4a26fc8",
+          "version": "0.1.1"
         }
       },
       {

+ 1 - 1
FreeAPS/Resources/Config.xcconfig

@@ -1 +1 @@
-BUILD_VERSION = 0.1.13
+BUILD_VERSION = 0.1.14

+ 4 - 4
FreeAPS/Sources/APS/APSManager.swift

@@ -292,7 +292,7 @@ final class BaseAPSManager: APSManager, Injectable {
 
         pump.enactBolus(units: roundedAmout, automatic: isSMB).sink { completion in
             if case let .failure(error) = completion {
-                debug(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
+                warning(.apsManager, "Bolus failed with error: \(error.localizedDescription)")
                 self.processError(APSError.pumpError(error))
             } else {
                 debug(.apsManager, "Bolus succeeded")
@@ -463,7 +463,7 @@ final class BaseAPSManager: APSManager, Injectable {
         guard let pump = pumpManager, verifyStatus() else {
             isLooping.send(false)
             debug(.apsManager, "Invalid pump state")
-            processError(APSError.invalidPumpState(message: "Pump is bolusing or suspended"))
+            processError(APSError.invalidPumpState(message: "Pump is busy, suspended or not set"))
             return
         }
 
@@ -493,7 +493,7 @@ final class BaseAPSManager: APSManager, Injectable {
             .flatMap { bolusPublisher }
             .sink { [weak self] completion in
                 if case let .failure(error) = completion {
-                    debug(.apsManager, "Loop failed with error: \(error.localizedDescription)")
+                    warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
                     self?.reportEnacted(suggestion: suggested, received: false)
                     self?.processError(APSError.pumpError(error))
                 } else {
@@ -524,7 +524,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func processError(_ error: Error) {
-        debug(.apsManager, "\(error.localizedDescription)")
+        warning(.apsManager, "\(error.localizedDescription)")
         lastError.send(error)
     }
 

+ 0 - 1
FreeAPS/Sources/Logger/Logger.swift

@@ -246,7 +246,6 @@ final class Logger {
         reporter.log(category.name, message, file: file, function: function, line: line)
         if !LoggerTestMode, maybeError?.shouldReportNonFatalIssue ?? true {
             reporter.reportNonFatalIssue(withError: loggerError.asNSError())
-            showAlert(message)
         }
     }
 

+ 1 - 1
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -25,7 +25,7 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
     let direction: Direction?
     let date: Decimal
     let dateString: Date
-    let filtered: Double?
+    let filtered: Decimal?
     let noise: Int?
 
     var glucose: Int?

+ 1 - 0
FreeAPS/Sources/Modules/Bolus/BolusDataFlow.swift

@@ -4,4 +4,5 @@ enum Bolus {
 
 protocol BolusProvider: Provider {
     var suggestion: Suggestion? { get }
+    func pumpSettings() -> PumpSettings
 }

+ 6 - 0
FreeAPS/Sources/Modules/Bolus/BolusProvider.swift

@@ -3,5 +3,11 @@ extension Bolus {
         var suggestion: Suggestion? {
             storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
         }
+
+        func pumpSettings() -> PumpSettings {
+            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+                ?? PumpSettings(insulinActionCurve: 5, maxBolus: 10, maxBasal: 2)
+        }
     }
 }

+ 4 - 1
FreeAPS/Sources/Modules/Bolus/BolusViewModel.swift

@@ -38,9 +38,12 @@ extension Bolus {
                 showModal(for: nil)
                 return
             }
+
+            let maxAmount = Double(min(amount, provider.pumpSettings().maxBolus))
+
             unlockmanager.unlock()
                 .sink { _ in } receiveValue: {
-                    self.apsManager.enactBolus(amount: Double(self.amount), isSMB: false)
+                    self.apsManager.enactBolus(amount: maxAmount, isSMB: false)
                     self.showModal(for: nil)
                 }
                 .store(in: &lifetime)

+ 1 - 0
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -12,6 +12,7 @@ protocol HomeProvider: Provider {
     func filteredGlucose(hours: Int) -> [BloodGlucose]
     func pumpHistory(hours: Int) -> [PumpHistoryEvent]
     func pumpSettings() -> PumpSettings
+    func autotunedBasalProfile() -> [BasalProfileEntry]
     func basalProfile() -> [BasalProfileEntry]
     func tempTargets(hours: Int) -> [TempTarget]
     func carbs(hours: Int) -> [CarbsEntry]

+ 6 - 1
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -64,10 +64,15 @@ extension Home {
             storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self)
         }
 
-        func basalProfile() -> [BasalProfileEntry] {
+        func autotunedBasalProfile() -> [BasalProfileEntry] {
             storage.retrieve(OpenAPS.Settings.profile, as: Autotune.self)?.basalProfile
                 ?? storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
                 ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
         }
+
+        func basalProfile() -> [BasalProfileEntry] {
+            storage.retrieve(OpenAPS.Settings.pumpProfile, as: Autotune.self)?.basalProfile
+                ?? [BasalProfileEntry(start: "00:00", minutes: 0, rate: 1)]
+        }
     }
 }

+ 15 - 0
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -18,12 +18,15 @@ extension Home {
         @Published var glucoseDelta: Int?
         @Published var tempBasals: [PumpHistoryEvent] = []
         @Published var boluses: [PumpHistoryEvent] = []
+        @Published var suspensions: [PumpHistoryEvent] = []
         @Published var maxBasal: Decimal = 2
+        @Published var autotunedBasalProfile: [BasalProfileEntry] = []
         @Published var basalProfile: [BasalProfileEntry] = []
         @Published var tempTargets: [TempTarget] = []
         @Published var carbs: [CarbsEntry] = []
         @Published var timerDate = Date()
         @Published var closedLoop = false
+        @Published var pumpSuspended = false
         @Published var isLooping = false
         @Published var statusTitle = ""
         @Published var lastLoopDate: Date = .distantPast
@@ -45,6 +48,7 @@ extension Home {
             setupGlucose()
             setupBasals()
             setupBoluses()
+            setupSuspensions()
             setupPumpSettings()
             setupBasalProfile()
             setupTempTargets()
@@ -173,6 +177,15 @@ extension Home {
             }
         }
 
+        private func setupSuspensions() {
+            DispatchQueue.main.async {
+                self.suspensions = self.provider.pumpHistory(hours: self.filteredHours).filter {
+                    $0.type == .pumpSuspend || $0.type == .pumpResume
+                }
+                self.pumpSuspended = self.suspensions.last?.type == .pumpSuspend
+            }
+        }
+
         private func setupPumpSettings() {
             DispatchQueue.main.async {
                 self.maxBasal = self.provider.pumpSettings().maxBasal
@@ -181,6 +194,7 @@ extension Home {
 
         private func setupBasalProfile() {
             DispatchQueue.main.async {
+                self.autotunedBasalProfile = self.provider.autotunedBasalProfile()
                 self.basalProfile = self.provider.basalProfile()
             }
         }
@@ -279,6 +293,7 @@ extension Home.ViewModel:
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
         setupBasals()
         setupBoluses()
+        setupSuspensions()
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {

+ 69 - 12
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -37,8 +37,10 @@ struct MainChartView: View {
     @Binding var suggestion: Suggestion?
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
+    @Binding var suspensions: [PumpHistoryEvent]
     @Binding var hours: Int
     @Binding var maxBasal: Decimal
+    @Binding var autotunedBasalProfile: [BasalProfileEntry]
     @Binding var basalProfile: [BasalProfileEntry]
     @Binding var tempTargets: [TempTarget]
     @Binding var carbs: [CarbsEntry]
@@ -53,6 +55,7 @@ struct MainChartView: View {
     @State private var tempBasalPath = Path()
     @State private var regularBasalPath = Path()
     @State private var tempTargetsPath = Path()
+    @State private var suspensionsPath = Path()
     @State private var carbsDots: [DotInfo] = []
     @State private var carbsPath = Path()
     @State private var glucoseYGange: GlucoseYRange = (0, 0, 0, 0)
@@ -162,6 +165,7 @@ struct MainChartView: View {
         ZStack {
             tempBasalPath.fill(Color.tempBasal)
             tempBasalPath.stroke(Color.tempBasal, lineWidth: 1)
+            suspensionsPath.fill(Color.loopGray)
             regularBasalPath.stroke(Color.basal, lineWidth: 1)
         }
         .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
@@ -170,10 +174,13 @@ struct MainChartView: View {
         .onChange(of: tempBasals) { _ in
             calculateBasalPoints(fullSize: fullSize)
         }
+        .onChange(of: suspensions) { _ in
+            calculateSuspensions(fullSize: fullSize)
+        }
         .onChange(of: maxBasal) { _ in
             calculateBasalPoints(fullSize: fullSize)
         }
-        .onChange(of: basalProfile) { _ in
+        .onChange(of: autotunedBasalProfile) { _ in
             calculateBasalPoints(fullSize: fullSize)
         }
         .onChange(of: didAppearTrigger) { _ in
@@ -360,6 +367,7 @@ extension MainChartView {
         calculateTempTargetsRects(fullSize: fullSize)
         calculateTempTargetsRects(fullSize: fullSize)
         calculateBasalPoints(fullSize: fullSize)
+        calculateSuspensions(fullSize: fullSize)
     }
 
     private func calculateGlucoseDots(fullSize: CGSize) {
@@ -458,7 +466,8 @@ extension MainChartView {
             let firstRegularBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
                 timeEnd: firstTempTime,
-                fullSize: fullSize
+                fullSize: fullSize,
+                autotuned: false
             )
             let tempBasalPoints = firstRegularBasalPoints + tempBasals.chunks(ofCount: 2).map { chunk -> [CGPoint] in
                 let chunk = Array(chunk)
@@ -468,7 +477,12 @@ extension MainChartView {
                 let rateCost = Config.basalHeight / CGFloat(maxBasalRate())
                 let x0 = timeToXCoordinate(timeBegin, fullSize: fullSize)
                 let y0 = Config.basalHeight - CGFloat(chunk[0].rate ?? 0) * rateCost
-                let regularPoints = findRegularBasalPoints(timeBegin: lastTimeEnd, timeEnd: timeBegin, fullSize: fullSize)
+                let regularPoints = findRegularBasalPoints(
+                    timeBegin: lastTimeEnd,
+                    timeEnd: timeBegin,
+                    fullSize: fullSize,
+                    autotuned: false
+                )
                 lastTimeEnd = timeEnd
                 return regularPoints + [CGPoint(x: x0, y: y0)]
             }.flatMap { $0 }
@@ -488,17 +502,18 @@ extension MainChartView {
             }
 
             let endDateTime = dayAgoTime + 1.days.timeInterval + 6.hours.timeInterval
-            let regularBasalPoints = findRegularBasalPoints(
+            let autotunedBasalPoints = findRegularBasalPoints(
                 timeBegin: dayAgoTime,
                 timeEnd: endDateTime,
-                fullSize: fullSize
+                fullSize: fullSize,
+                autotuned: true
             )
 
-            let regularBasalPath = Path { path in
+            let autotunedBasalPath = Path { path in
                 var yPoint: CGFloat = Config.basalHeight
                 path.move(to: CGPoint(x: -50, y: yPoint))
 
-                for point in regularBasalPoints {
+                for point in autotunedBasalPoints {
                     path.addLine(to: CGPoint(x: point.x, y: yPoint))
                     path.addLine(to: point)
                     yPoint = point.y
@@ -508,7 +523,42 @@ extension MainChartView {
 
             DispatchQueue.main.async {
                 self.tempBasalPath = tempBasalPath
-                self.regularBasalPath = regularBasalPath
+                self.regularBasalPath = autotunedBasalPath
+            }
+        }
+    }
+
+    private func calculateSuspensions(fullSize: CGSize) {
+        calculationQueue.async {
+            var rects = suspensions.windows(ofCount: 2).map { window -> CGRect? in
+                let window = Array(window)
+                guard window[0].type == .pumpSuspend, window[1].type == .pumpResume else { return nil }
+                let x0 = self.timeToXCoordinate(window[0].timestamp.timeIntervalSince1970, fullSize: fullSize)
+                let x1 = self.timeToXCoordinate(window[1].timestamp.timeIntervalSince1970, fullSize: fullSize)
+                return CGRect(x: x0, y: 0, width: x1 - x0, height: Config.basalHeight)
+            }
+
+            let firstRec = self.suspensions.first.flatMap { event -> CGRect? in
+                guard event.type == .pumpResume else { return nil }
+                let width = self.timeToXCoordinate(event.timestamp.timeIntervalSince1970, fullSize: fullSize)
+                return CGRect(x: 0, y: 0, width: width, height: Config.basalHeight)
+            }
+
+            let lastRec = self.suspensions.last.flatMap { event -> CGRect? in
+                guard event.type == .pumpSuspend else { return nil }
+                let x0 = self.timeToXCoordinate(event.timestamp.timeIntervalSince1970, fullSize: fullSize)
+                let x1 = self.fullGlucoseWidth(viewWidth: fullSize.width) + self.additionalWidth(viewWidth: fullSize.width)
+                return CGRect(x: x0, y: 0, width: x1 - x0, height: Config.basalHeight)
+            }
+            rects.append(firstRec)
+            rects.append(lastRec)
+
+            let path = Path { path in
+                path.addRects(rects.compactMap { $0 })
+            }
+
+            DispatchQueue.main.async {
+                suspensionsPath = path
             }
         }
     }
@@ -560,7 +610,12 @@ extension MainChartView {
         }
     }
 
-    private func findRegularBasalPoints(timeBegin: TimeInterval, timeEnd: TimeInterval, fullSize: CGSize) -> [CGPoint] {
+    private func findRegularBasalPoints(
+        timeBegin: TimeInterval,
+        timeEnd: TimeInterval,
+        fullSize: CGSize,
+        autotuned: Bool
+    ) -> [CGPoint] {
         guard timeBegin < timeEnd else {
             return []
         }
@@ -568,17 +623,19 @@ extension MainChartView {
         let calendar = Calendar.current
         let startOfDay = calendar.startOfDay(for: beginDate)
 
-        let basalNormalized = basalProfile.map {
+        let profile = autotuned ? autotunedBasalProfile : basalProfile
+
+        let basalNormalized = profile.map {
             (
                 time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval).timeIntervalSince1970,
                 rate: $0.rate
             )
-        } + basalProfile.map {
+        } + profile.map {
             (
                 time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval + 1.days.timeInterval).timeIntervalSince1970,
                 rate: $0.rate
             )
-        } + basalProfile.map {
+        } + profile.map {
             (
                 time: startOfDay.addingTimeInterval($0.minutes.minutes.timeInterval + 2.days.timeInterval).timeIntervalSince1970,
                 rate: $0.rate

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

@@ -55,7 +55,7 @@ struct CurrentGlucoseView: View {
                         } ??
                         "--"
 
-                ).font(.caption2).foregroundColor(.secondary)
+                ).font(.system(size: 12, weight: .bold))
             }
         }
     }

+ 7 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -86,7 +86,11 @@ extension Home {
 
         var infoPanal: some View {
             HStack(alignment: .center) {
-                if let tempRate = viewModel.tempRate {
+                if viewModel.pumpSuspended {
+                    Text("Pump suspended")
+                        .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGray)
+                        .padding(.leading, 8)
+                } else if let tempRate = viewModel.tempRate {
                     Text((numberFormatter.string(from: tempRate as NSNumber) ?? "0") + " U/hr")
                         .font(.system(size: 12, weight: .bold)).foregroundColor(.insulin)
                         .padding(.leading, 8)
@@ -190,8 +194,10 @@ extension Home {
                         suggestion: $viewModel.suggestion,
                         tempBasals: $viewModel.tempBasals,
                         boluses: $viewModel.boluses,
+                        suspensions: $viewModel.suspensions,
                         hours: .constant(viewModel.filteredHours),
                         maxBasal: $viewModel.maxBasal,
+                        autotunedBasalProfile: $viewModel.autotunedBasalProfile,
                         basalProfile: $viewModel.basalProfile,
                         tempTargets: $viewModel.tempTargets,
                         carbs: $viewModel.carbs,

+ 12 - 0
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -77,6 +77,10 @@ extension NightscoutAPI {
         return service.run(request)
             .retry(Config.retryCount)
             .decode(type: [BloodGlucose].self, decoder: JSONCoding.decoder)
+            .catch { error -> AnyPublisher<[BloodGlucose], Swift.Error> in
+                warning(.nightscout, "Glucose fetching error: \(error.localizedDescription)")
+                return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
+            }
             .map { glucose in
                 glucose
                     .map {
@@ -124,6 +128,10 @@ extension NightscoutAPI {
         return service.run(request)
             .retry(Config.retryCount)
             .decode(type: [CarbsEntry].self, decoder: JSONCoding.decoder)
+            .catch { error -> AnyPublisher<[CarbsEntry], Swift.Error> in
+                warning(.nightscout, "Carbs fetching error: \(error.localizedDescription)")
+                return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
+            }
             .eraseToAnyPublisher()
     }
 
@@ -193,6 +201,10 @@ extension NightscoutAPI {
         return service.run(request)
             .retry(Config.retryCount)
             .decode(type: [TempTarget].self, decoder: JSONCoding.decoder)
+            .catch { error -> AnyPublisher<[TempTarget], Swift.Error> in
+                warning(.nightscout, "TempTarget fetching error: \(error.localizedDescription)")
+                return Just([]).setFailureType(to: Swift.Error.self).eraseToAnyPublisher()
+            }
             .eraseToAnyPublisher()
     }
 

+ 1 - 1
FreeAPS/Sources/Services/UnlockManager/UnlockManager.swift

@@ -23,7 +23,7 @@ final class BaseUnlockManager: UnlockManager {
                 }
             }
 
-            let reason = "We need to unlock your data."
+            let reason = "We need to make sure you are the owner of the device."
 
             if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
                 context.evaluatePolicy(