Ivan Valkou 5 лет назад
Родитель
Сommit
ba79094615

+ 7 - 0
FreeAPS/Sources/Helpers/Decimal+Extensions.swift

@@ -1,3 +1,4 @@
+import CoreGraphics
 import Foundation
 import Foundation
 
 
 extension Double {
 extension Double {
@@ -11,3 +12,9 @@ extension Int {
         self.init(Double(decimal))
         self.init(Double(decimal))
     }
     }
 }
 }
+
+extension CGFloat {
+    init(_ decimal: Decimal) {
+        self.init(Double(decimal))
+    }
+}

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

@@ -1,6 +1,6 @@
 import Foundation
 import Foundation
 
 
-struct PumpHistoryEvent: JSON {
+struct PumpHistoryEvent: JSON, Equatable {
     let id: String
     let id: String
     let type: EventType
     let type: EventType
     let timestamp: Date
     let timestamp: Date

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

@@ -9,4 +9,5 @@ protocol HomeProvider: Provider {
     func fetchAndLoop()
     func fetchAndLoop()
     func filteredGlucose(hours: Int) -> [BloodGlucose]
     func filteredGlucose(hours: Int) -> [BloodGlucose]
     func pumpHistory(hours: Int) -> [PumpHistoryEvent]
     func pumpHistory(hours: Int) -> [PumpHistoryEvent]
+    func pumpSettings() -> PumpSettings
 }
 }

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

@@ -26,5 +26,11 @@ extension Home {
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()
                 $0.timestamp.addingTimeInterval(hours.hours.timeInterval) > Date()
             }
             }
         }
         }
+
+        func pumpSettings() -> PumpSettings {
+            (try? storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self))
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+                ?? PumpSettings(insulinActionCurve: 5, maxBolus: 10, maxBasal: 2)
+        }
     }
     }
 }
 }

+ 26 - 9
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -13,6 +13,7 @@ extension Home {
         @Published var recentGlucose: BloodGlucose?
         @Published var recentGlucose: BloodGlucose?
         @Published var glucoseDelta: Int?
         @Published var glucoseDelta: Int?
         @Published var basals: [PumpHistoryEvent] = []
         @Published var basals: [PumpHistoryEvent] = []
+        @Published var maxBasal: Decimal = 2
 
 
         @Published var allowManualTemp = false
         @Published var allowManualTemp = false
         private(set) var units: GlucoseUnits = .mmolL
         private(set) var units: GlucoseUnits = .mmolL
@@ -20,6 +21,7 @@ extension Home {
         override func subscribe() {
         override func subscribe() {
             setupGlucose()
             setupGlucose()
             setupBasals()
             setupBasals()
+            setupPumpSettings()
             suggestion = provider.suggestion
             suggestion = provider.suggestion
             units = settingsManager.settings.units
             units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
             allowManualTemp = !settingsManager.settings.closedLoop
@@ -27,6 +29,7 @@ extension Home {
             broadcaster.register(SuggestionObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(PumpHistoryObserver.self, observer: self)
             broadcaster.register(PumpHistoryObserver.self, observer: self)
+            broadcaster.register(PumpSettingsObserver.self, observer: self)
         }
         }
 
 
         func addCarbs() {
         func addCarbs() {
@@ -58,24 +61,34 @@ extension Home {
         }
         }
 
 
         private func setupGlucose() {
         private func setupGlucose() {
-            glucose = provider.filteredGlucose(hours: filteredHours)
-            recentGlucose = glucose.last
-            if glucose.count >= 2 {
-                glucoseDelta = (recentGlucose?.glucose ?? 0) - (glucose[glucose.count - 2].glucose ?? 0)
-            } else {
-                glucoseDelta = nil
+            DispatchQueue.main.async {
+                self.glucose = self.provider.filteredGlucose(hours: self.filteredHours)
+                self.recentGlucose = self.glucose.last
+                if self.glucose.count >= 2 {
+                    self.glucoseDelta = (self.recentGlucose?.glucose ?? 0) - (self.glucose[self.glucose.count - 2].glucose ?? 0)
+                } else {
+                    self.glucoseDelta = nil
+                }
             }
             }
         }
         }
 
 
         private func setupBasals() {
         private func setupBasals() {
-            basals = provider.pumpHistory(hours: filteredHours).filter {
-                $0.type == .tempBasal || $0.type == .tempBasalDuration
+            DispatchQueue.main.async {
+                self.basals = self.provider.pumpHistory(hours: self.filteredHours).filter {
+                    $0.type == .tempBasal || $0.type == .tempBasalDuration
+                }
+            }
+        }
+
+        private func setupPumpSettings() {
+            DispatchQueue.main.async {
+                self.maxBasal = self.provider.pumpSettings().maxBasal
             }
             }
         }
         }
     }
     }
 }
 }
 
 
-extension Home.ViewModel: GlucoseObserver, SuggestionObserver, SettingsObserver, PumpHistoryObserver {
+extension Home.ViewModel: GlucoseObserver, SuggestionObserver, SettingsObserver, PumpHistoryObserver, PumpSettingsObserver {
     func glucoseDidUpdate(_: [BloodGlucose]) {
     func glucoseDidUpdate(_: [BloodGlucose]) {
         setupGlucose()
         setupGlucose()
     }
     }
@@ -91,4 +104,8 @@ extension Home.ViewModel: GlucoseObserver, SuggestionObserver, SettingsObserver,
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
         setupBasals()
         setupBasals()
     }
     }
+
+    func pumpSettingsDidChange(_: PumpSettings) {
+        setupPumpSettings()
+    }
 }
 }

+ 86 - 9
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -23,11 +23,14 @@ struct MainChartView: View {
     @Binding var suggestion: Suggestion?
     @Binding var suggestion: Suggestion?
     @Binding var basals: [PumpHistoryEvent]
     @Binding var basals: [PumpHistoryEvent]
     @Binding var hours: Int
     @Binding var hours: Int
+    @Binding var maxBasal: Decimal
     let units: GlucoseUnits
     let units: GlucoseUnits
 
 
     @State var didAppearTrigger = false
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
     @State private var glucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
+    @State private var basalPoints: [CGPoint] = []
+    @State private var basalPath = Path()
 
 
     private var dateDormatter: DateFormatter {
     private var dateDormatter: DateFormatter {
         let formatter = DateFormatter()
         let formatter = DateFormatter()
@@ -42,6 +45,13 @@ struct MainChartView: View {
         return formatter
         return formatter
     }
     }
 
 
+    private var basalFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        return formatter
+    }
+
     // MARK: - Views
     // MARK: - Views
 
 
     var body: some View {
     var body: some View {
@@ -61,14 +71,20 @@ struct MainChartView: View {
                     ScrollViewReader { scroll in
                     ScrollViewReader { scroll in
                         ZStack(alignment: .top) {
                         ZStack(alignment: .top) {
                             basalChart(fullSize: geo.size)
                             basalChart(fullSize: geo.size)
-                            mainChart(fullSize: geo.size)
+                            mainChart(fullSize: geo.size).id("End")
                                 .onChange(of: glucose) { _ in
                                 .onChange(of: glucose) { _ in
-                                    scroll.scrollTo("End")
+                                    scroll.scrollTo("End", anchor: .trailing)
+                                }
+                                .onChange(of: suggestion) { _ in
+                                    scroll.scrollTo("End", anchor: .trailing)
+                                }
+                                .onChange(of: basals) { _ in
+                                    scroll.scrollTo("End", anchor: .trailing)
                                 }
                                 }
                                 .onAppear {
                                 .onAppear {
-                                    scroll.scrollTo("End")
                                     // add trigger to the end of main queue
                                     // add trigger to the end of main queue
                                     DispatchQueue.main.async {
                                     DispatchQueue.main.async {
+                                        scroll.scrollTo("End", anchor: .trailing)
                                         didAppearTrigger = true
                                         didAppearTrigger = true
                                     }
                                     }
                                 }
                                 }
@@ -76,7 +92,7 @@ struct MainChartView: View {
                     }
                     }
                 }
                 }
                 // Y glucose labels
                 // Y glucose labels
-                ForEach(0 ..< Config.yLinesCount) { line -> AnyView in
+                ForEach(0 ..< Config.yLinesCount + 1) { line -> AnyView in
                     let range = glucoseYRange(fullSize: geo.size)
                     let range = glucoseYRange(fullSize: geo.size)
                     let yStep = (range.maxY - range.minY) / CGFloat(Config.yLinesCount)
                     let yStep = (range.maxY - range.minY) / CGFloat(Config.yLinesCount)
                     let valueStep = Double(range.maxValue - range.minValue) / Double(Config.yLinesCount)
                     let valueStep = Double(range.maxValue - range.minValue) / Double(Config.yLinesCount)
@@ -93,11 +109,27 @@ struct MainChartView: View {
     }
     }
 
 
     private func basalChart(fullSize: CGSize) -> some View {
     private func basalChart(fullSize: CGSize) -> some View {
-        Group {
-            EmptyView()
+        ZStack {
+            basalPath.fill(Color.blue)
+            basalPath.stroke(Color.blue, lineWidth: 1)
+            Text(lastBasalRateString)
+                .foregroundColor(.blue)
+                .font(.caption2)
+                .position(CGPoint(x: lastBasalPoint(fullSize: fullSize).x + 25, y: Config.basalHeight / 2))
         }
         }
+        .drawingGroup()
         .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
         .frame(width: fullGlucoseWidth(viewWidth: fullSize.width) + additionalWidth(viewWidth: fullSize.width))
-        .frame(maxHeight: Config.basalHeight).background(Color.secondary.opacity(0.1))
+        .frame(maxHeight: Config.basalHeight)
+        .background(Color.secondary.opacity(0.1))
+        .onChange(of: basals) { _ in
+            calculateBasalPoints(fullSize: fullSize)
+        }
+        .onChange(of: maxBasal) { _ in
+            calculateBasalPoints(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            calculateBasalPoints(fullSize: fullSize)
+        }
     }
     }
 
 
     private func mainChart(fullSize: CGSize) -> some View {
     private func mainChart(fullSize: CGSize) -> some View {
@@ -116,7 +148,7 @@ struct MainChartView: View {
                     }
                     }
                     .stroke(Color.secondary, lineWidth: 0.2)
                     .stroke(Color.secondary, lineWidth: 0.2)
                     glucosePath(fullSize: fullSize)
                     glucosePath(fullSize: fullSize)
-                    predictions(fullSize: fullSize).id("End")
+                    predictions(fullSize: fullSize)
                 }
                 }
                 ZStack {
                 ZStack {
                     // X time labels
                     // X time labels
@@ -223,7 +255,52 @@ struct MainChartView: View {
         }
         }
     }
     }
 
 
-    private func calculateBasalPoints(fullSize _: CGSize) {}
+    private func calculateBasalPoints(fullSize: CGSize) {
+        basalPoints = basals.chunks(ofCount: 2).compactMap { chunk -> CGPoint? in
+            let chunk = Array(chunk)
+            guard chunk.count == 2, chunk[0].type == .tempBasal, chunk[1].type == .tempBasalDuration else { return nil }
+            let timeBegin = chunk[0].timestamp
+            let rateCost = Config.basalHeight / CGFloat(maxBasal)
+            let x = timeToXCoordinate(timeBegin.timeIntervalSince1970, fullSize: fullSize)
+            let y = Config.basalHeight - CGFloat(chunk[0].rate ?? 0) * rateCost
+            return CGPoint(x: x, y: y)
+        }
+        basalPath = Path { path in
+            var yPoint: CGFloat = Config.basalHeight
+            path.move(to: CGPoint(x: 0, y: yPoint))
+
+            for point in basalPoints {
+                path.addLine(to: CGPoint(x: point.x, y: yPoint))
+                path.addLine(to: point)
+                yPoint = point.y
+            }
+            let lastPoint = lastBasalPoint(fullSize: fullSize)
+            path.addLine(to: lastPoint)
+            path.addLine(to: CGPoint(x: lastPoint.x, y: Config.basalHeight))
+            path.addLine(to: CGPoint(x: 0, y: Config.basalHeight))
+        }
+    }
+
+    private func lastBasalPoint(fullSize: CGSize) -> CGPoint {
+        let lastBasal = Array(basals.suffix(2))
+        guard lastBasal.count == 2 else {
+            return .zero
+        }
+        let endBasalTime = lastBasal[0].timestamp.timeIntervalSince1970 + (lastBasal[1].durationMin?.minutes.timeInterval ?? 0)
+        let rateCost = Config.basalHeight / CGFloat(maxBasal)
+        let x = timeToXCoordinate(endBasalTime, fullSize: fullSize)
+        let y = Config.basalHeight - CGFloat(lastBasal[0].rate ?? 0) * rateCost
+        return CGPoint(x: x, y: y)
+    }
+
+    private var lastBasalRateString: String {
+        let lastBasal = Array(basals.suffix(2))
+        guard lastBasal.count == 2 else {
+            return ""
+        }
+        let lastRate = lastBasal[0].rate ?? 0
+        return (basalFormatter.string(from: lastRate as NSNumber) ?? "0") + " U/h"
+    }
 
 
     private func fullGlucoseWidth(viewWidth: CGFloat) -> CGFloat {
     private func fullGlucoseWidth(viewWidth: CGFloat) -> CGFloat {
         viewWidth * CGFloat(hours) / CGFloat(Config.screenHours)
         viewWidth * CGFloat(hours) / CGFloat(Config.screenHours)

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

@@ -52,6 +52,7 @@ extension Home {
                         suggestion: $viewModel.suggestion,
                         suggestion: $viewModel.suggestion,
                         basals: $viewModel.basals,
                         basals: $viewModel.basals,
                         hours: .constant(viewModel.filteredHours),
                         hours: .constant(viewModel.filteredHours),
+                        maxBasal: $viewModel.maxBasal,
                         units: viewModel.units
                         units: viewModel.units
                     )
                     )
 
 

+ 16 - 2
FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorProvider.swift

@@ -2,9 +2,14 @@ import Combine
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
 
 
+protocol PumpSettingsObserver {
+    func pumpSettingsDidChange(_ pumpSettings: PumpSettings)
+}
+
 extension PumpSettingsEditor {
 extension PumpSettingsEditor {
     final class Provider: BaseProvider, PumpSettingsEditorProvider {
     final class Provider: BaseProvider, PumpSettingsEditorProvider {
         private let processQueue = DispatchQueue(label: "PumpSettingsEditorProvider.processQueue")
         private let processQueue = DispatchQueue(label: "PumpSettingsEditorProvider.processQueue")
+        @Injected() private var broadcaster: Broadcaster!
 
 
         func settings() -> PumpSettings {
         func settings() -> PumpSettings {
             (try? storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self))
             (try? storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self))
@@ -13,8 +18,17 @@ extension PumpSettingsEditor {
         }
         }
 
 
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
-            guard let pump = deviceManager?.pumpManager else {
+            func save() {
                 try? storage.save(settings, as: OpenAPS.Settings.settings)
                 try? storage.save(settings, as: OpenAPS.Settings.settings)
+                processQueue.async {
+                    self.broadcaster.notify(PumpSettingsObserver.self, on: self.processQueue) {
+                        $0.pumpSettingsDidChange(settings)
+                    }
+                }
+            }
+
+            guard let pump = deviceManager?.pumpManager else {
+                save()
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
                 return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
             }
             }
             // Don't ask why 🤦‍♂️
             // Don't ask why 🤦‍♂️
@@ -26,7 +40,7 @@ extension PumpSettingsEditor {
                     pump.syncDeliveryLimitSettings(for: sync) { result in
                     pump.syncDeliveryLimitSettings(for: sync) { result in
                         switch result {
                         switch result {
                         case .success:
                         case .success:
-                            try? self.storage.save(settings, as: OpenAPS.Settings.settings)
+                            save()
                             promise(.success(()))
                             promise(.success(()))
                         case let .failure(error):
                         case let .failure(error):
                             promise(.failure(error))
                             promise(.failure(error))

+ 1 - 0
FreeAPS/Sources/Modules/PumpSettingsEditor/PumpSettingsEditorViewModel.swift

@@ -26,6 +26,7 @@ extension PumpSettingsEditor {
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
                 .sink { _ in
                 .sink { _ in
                     self.syncInProgress = false
                     self.syncInProgress = false
+
                 } receiveValue: {}
                 } receiveValue: {}
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }