Selaa lähdekoodia

Merge remote-tracking branch 'trixing/patch/autotune' into bdb

Jon B.M 4 vuotta sitten
vanhempi
commit
47f482bd0b

+ 3 - 0
Dependencies/LoopKit/LoopKit/DeviceManager/PumpManager.swift

@@ -183,6 +183,9 @@ public protocol PumpManager: DeviceManager {
     ///   - completion: A closure called after the command is complete
     ///   - result: A BasalRateSchedule or an error describing why the command failed
     func syncBasalRateSchedule(items scheduleItems: [RepeatingScheduleValue<Double>], completion: @escaping (_ result: Result<BasalRateSchedule, Error>) -> Void)
+
+    func getBasalRateSchedule(completion: @escaping (Result<BasalRateSchedule, Error>) -> Void)
+
 }
 
 

+ 12 - 0
Dependencies/LoopKit/MockKit/MockPumpManager.swift

@@ -590,6 +590,18 @@ public final class MockPumpManager: TestingPumpManager {
             completion(.success(BasalRateSchedule(dailyItems: scheduleItems, timeZone: self.status.timeZone)!))
         }
     }
+
+    public func getBasalRateSchedule(completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
+        if let schedule = state.basalRateSchedule {
+            NSLog("Mock read success")
+
+            completion(.success(BasalRateSchedule(dailyItems: schedule.items, timeZone: self.status.timeZone)!))
+        } else {
+            NSLog("Mock read failure")
+            completion(.failure(PumpManagerError.communication(MockPumpManagerError.communicationFailure)))
+        }
+    }
+
 }
 
 // MARK: - AlertResponder implementation

+ 26 - 0
Dependencies/rileylink_ios/MinimedKit/PumpManager/MinimedPumpManager.swift

@@ -1244,6 +1244,32 @@ extension MinimedPumpManager: PumpManager {
             }
         }
     }
+
+    public func getBasalRateSchedule(completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
+        pumpOps.runSession(withName: "Save Basal Profile", using: rileyLinkDeviceProvider.firstConnectedDevice) { (session) in
+            guard let session = session else {
+                completion(.failure(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)))
+                return
+            }
+
+            do {
+                if let schedule = try session.getBasalSchedule() {
+                    let dailyItems = schedule.entries.map {
+                        RepeatingScheduleValue<Double>(
+                            startTime: $0.timeOffset,
+                            value: $0.rate)
+                    }
+                    completion(.success(BasalRateSchedule(dailyItems: dailyItems, timeZone: session.pump.timeZone)!))
+                } else {
+                    self.log.error("Get basal profile return no result")
+                    completion(.failure(PumpManagerError.communication("Get Basal failed"  as? LocalizedError)))
+                }
+            } catch let error {
+                self.log.error("Get basal profile failed: %{public}@", String(describing: error))
+                completion(.failure(error))
+            }
+        }
+    }
 }
 
 extension MinimedPumpManager: PumpOpsDelegate {

+ 11 - 0
Dependencies/rileylink_ios/MinimedKitUI/MinimedPumpManager+UI.swift

@@ -109,6 +109,17 @@ extension MinimedPumpManager {
         }
     }
 
+    public func getScheduleValues(for viewController: BasalScheduleTableViewController, completion: @escaping (SyncBasalScheduleResult<Double>) -> Void) {
+        getBasalRateSchedule() { result in
+            switch result {
+            case .success(let schedule):
+                completion(.success(scheduleItems: schedule.items, timeZone: schedule.timeZone))
+            case .failure(let error):
+                completion(.failure(error))
+            }
+        }
+    }
+
     public func syncButtonTitle(for viewController: BasalScheduleTableViewController) -> String {
         return LocalizedString("Save to Pump…", comment: "Title of button to save basal profile to pump")
     }

+ 5 - 0
Dependencies/rileylink_ios/OmniKit/PumpManager/OmnipodPumpManager.swift

@@ -1721,6 +1721,11 @@ extension OmnipodPumpManager: PumpManager {
         }
     }
 
+    public func getBasalRateSchedule(completion: @escaping (Result<BasalRateSchedule, Error>) -> Void) {
+        completion(.failure(PumpManagerError.deviceState(PodCommsError.podSuspended)))
+
+    }
+
     // This cannot be called from within the lockedState lock!
     func store(doses: [UnfinalizedDose], in session: PodCommsSession) -> Bool {
         session.assertOnSessionQueue()

+ 4 - 0
FreeAPS/Sources/Models/BasalProfileEntry.swift

@@ -17,6 +17,10 @@ extension BasalProfileEntry {
         case rate
     }
 
+    var displayTime: String {
+        String(start.prefix(5))
+    }
+
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         let start = try container.decode(String.self, forKey: .start)

+ 6 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigProvider.swift

@@ -8,6 +8,12 @@ extension AutotuneConfig {
             storage.retrieve(OpenAPS.Settings.autotune, as: Autotune.self)
         }
 
+        var basalProfilePump: [BasalProfileEntry] {
+            storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)
+                ?? [BasalProfileEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.basalProfile))
+                ?? []
+        }
+
         func runAutotune() -> AnyPublisher<Autotune?, Never> {
             apsManager.autotune()
         }

+ 58 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigStateModel.swift

@@ -1,4 +1,5 @@
 import Combine
+import LoopKit
 import SwiftUI
 
 extension AutotuneConfig {
@@ -6,6 +7,7 @@ extension AutotuneConfig {
         @Injected() var apsManager: APSManager!
         @Published var useAutotune = false
         @Published var autotune: Autotune?
+        @Published var basalProfile: [BasalProfileEntry?] = []
         private(set) var units: GlucoseUnits = .mmolL
         @Published var publishedDate = Date()
         @Persisted(key: "lastAutotuneDate") private var lastAutotuneDate = Date() {
@@ -18,6 +20,20 @@ extension AutotuneConfig {
 
         override func subscribe() {
             autotune = provider.autotune
+            var bp: [BasalProfileEntry?] = []
+            for p in provider.autotune?.basalProfile ?? [] {
+                var np: BasalProfileEntry?
+                for b in provider.basalProfilePump {
+                    if b.start > p.start {
+                        NSLog("Matched \(p) with \(b)")
+                        break
+                    }
+                    np = b
+                }
+                bp.append(np)
+            }
+            NSLog("basalProfile \(bp)")
+            basalProfile = bp
             units = settingsManager.settings.units
             useAutotune = settingsManager.settings.useAutotune
             publishedDate = lastAutotuneDate
@@ -57,5 +73,47 @@ extension AutotuneConfig {
                 .cancellable()
                 .store(in: &lifetime)
         }
+
+        func copyBasal() {
+            guard let autotuneProfile = autotune?.basalProfile else {
+                NSLog("copyBasal failure - no profile")
+                return
+            }
+            guard let pump = provider.deviceManager?.pumpManager else {
+                // storage.save(profile, as: OpenAPS.Settings.basalProfile)
+                NSLog("copyBasal failure - no pump")
+                return
+            }
+            let profile = autotuneProfile.map {
+                BasalProfileEntry(
+                    start: $0.start,
+                    minutes: $0.minutes,
+                    // Round to 0.05, ie. 1/20th
+                    rate: Decimal(round(Double($0.rate) * 20) / 20)
+                )
+            }
+            for item in profile {
+                NSLog("\(item.minutes) \(item.rate)")
+            }
+            let syncValues = profile.map {
+                RepeatingScheduleValue(
+                    startTime: TimeInterval($0.minutes * 60),
+                    value: Double($0.rate)
+                )
+            }
+
+            for item in syncValues {
+                NSLog("\(item.startTime) \(item.value)")
+            }
+            pump.syncBasalRateSchedule(items: syncValues) { result in
+                switch result {
+                case .success:
+                    NSLog("copyBasal success")
+                    self.provider.storage.save(profile, as: OpenAPS.Settings.basalProfile)
+                case let .failure(error):
+                    NSLog("copyBasal failed \(error)")
+                }
+            }
+        }
     }
 }

+ 27 - 4
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -66,10 +66,27 @@ extension AutotuneConfig {
                     Section(header: Text("Basal profile")) {
                         ForEach(0 ..< autotune.basalProfile.count, id: \.self) { index in
                             HStack {
-                                Text(autotune.basalProfile[index].start).foregroundColor(.secondary)
-                                Spacer()
-                                Text(rateFormatter.string(from: autotune.basalProfile[index].rate as NSNumber) ?? "0")
-                                Text("U/hr").foregroundColor(.secondary)
+                                VStack {
+                                    HStack {
+                                        Text(autotune.basalProfile[index].displayTime).foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(rateFormatter.string(from: autotune.basalProfile[index].rate as NSNumber) ?? "0")
+                                        Text("U/hr").foregroundColor(.secondary)
+                                    }
+                                    // To prevent a race condition if Autotune is ran an no profile
+                                    // existed before.
+                                    if state.basalProfile.indices.contains(index),
+                                       let basalProfile = state.basalProfile[index]
+                                    {
+                                        HStack {
+                                            Text("Pump")
+                                            Text(basalProfile.displayTime)
+                                            Spacer()
+                                            Text(rateFormatter.string(from: basalProfile.rate as NSNumber) ?? "0")
+                                            Text("U/hr").foregroundColor(.secondary)
+                                        }.font(Font.system(size: 12))
+                                    }
+                                }
                             }
                         }
                     }
@@ -79,6 +96,12 @@ extension AutotuneConfig {
                         label: { Text("Delete autotune data") }
                             .foregroundColor(.red)
                     }
+
+                    Section {
+                        Button { state.copyBasal() }
+                        label: { Text("Copy basal rates to pump") }
+                            .foregroundColor(.red)
+                    }
                 }
             }
             .onAppear(perform: configureView)

+ 1 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorDataFlow.swift

@@ -28,4 +28,5 @@ protocol BasalProfileEditorProvider: Provider {
     var profile: [BasalProfileEntry] { get }
     var supportedBasalRates: [Decimal]? { get }
     func saveProfile(_ profile: [BasalProfileEntry]) -> AnyPublisher<Void, Error>
+    func readProfile() -> AnyPublisher<Void, Error>
 }

+ 45 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorProvider.swift

@@ -12,6 +12,10 @@ extension BasalProfileEditor {
                 ?? []
         }
 
+        var autotune: Autotune? {
+            storage.retrieve(OpenAPS.Settings.autotune, as: Autotune.self)
+        }
+
         var supportedBasalRates: [Decimal]? {
             deviceManager.pumpManager?.supportedBasalRates.map { Decimal($0) }
         }
@@ -38,5 +42,46 @@ extension BasalProfileEditor {
                 }
             }.eraseToAnyPublisher()
         }
+
+        func readProfile() -> AnyPublisher<Void, Error> {
+            guard let pump = deviceManager?.pumpManager else {
+                // storage.save(profile, as: OpenAPS.Settings.basalProfile)
+                return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
+            }
+
+            // let syncValues = profile.map {
+            //    RepeatingScheduleValue(startTime: TimeInterval($0.minutes * 60), value: Double($0.rate))
+            // }
+
+            return Future { promise in
+                pump.getBasalRateSchedule { result in
+                    switch result {
+                    case let .success(scheduleItems):
+                        var newProfile: [BasalProfileEntry] = []
+                        for item in scheduleItems.items {
+                            NSLog("getBasalRateSchedule \(item.startTime) \(item.value)")
+                            let startMinutes = Int(item.startTime / 60) // seconds to minutes
+                            let start = String(format: "%2d:%2d", startMinutes / 60, startMinutes % 60)
+                            let rate = Decimal(item.value)
+                            newProfile.append(BasalProfileEntry(
+                                start: start,
+                                minutes: startMinutes,
+                                rate: rate
+                            ))
+                        }
+
+                        for p in newProfile {
+                            NSLog("getBasalRateSchedule \(p.start) \(p.minutes) \(p.rate)")
+                        }
+
+                        self.storage.save(newProfile, as: OpenAPS.Settings.basalProfile)
+                        // self.profile = newProfile
+                        promise(.success(()))
+                    case let .failure(error):
+                        promise(.failure(error))
+                    }
+                }
+            }.eraseToAnyPublisher()
+        }
     }
 }

+ 27 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -4,6 +4,7 @@ extension BasalProfileEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Published var syncInProgress = false
         @Published var items: [Item] = []
+        @Published var autotuneProfile: [BasalProfileEntry?] = []
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
@@ -22,6 +23,21 @@ extension BasalProfileEditor {
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
+
+            var bp: [BasalProfileEntry?] = []
+            for p in provider.profile {
+                var np: BasalProfileEntry?
+                for b in provider.autotune?.basalProfile ?? [] {
+                    if b.start > p.start {
+                        NSLog("Matched \(p) with \(b)")
+                        break
+                    }
+                    np = b
+                }
+                bp.append(np)
+            }
+            NSLog("basalProfile \(bp)")
+            autotuneProfile = bp
         }
 
         func add() {
@@ -56,6 +72,17 @@ extension BasalProfileEditor {
                 .store(in: &lifetime)
         }
 
+        func read() {
+            syncInProgress = true
+            provider.readProfile()
+                .receive(on: DispatchQueue.main)
+                .sink { _ in
+                    self.syncInProgress = false
+                    self.subscribe()
+                } receiveValue: {}
+                .store(in: &lifetime)
+        }
+
         func validate() {
             DispatchQueue.main.async {
                 let uniq = Array(Set(self.items))

+ 33 - 10
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -37,6 +37,17 @@ extension BasalProfileEditor {
                         }
                         .disabled(state.syncInProgress || state.items.isEmpty)
                     }
+
+                    HStack {
+                        if state.syncInProgress {
+                            ProgressView().padding(.trailing, 10)
+                        }
+                        Button { state.read() }
+                        label: {
+                            Text(state.syncInProgress ? "Readimg..." : "Read from Pump")
+                        }
+                        .disabled(state.syncInProgress || state.items.isEmpty)
+                    }
                 }
             }
             .onAppear(perform: configureView)
@@ -94,16 +105,28 @@ extension BasalProfileEditor {
             List {
                 ForEach(state.items.indexed(), id: \.1.id) { index, item in
                     NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text("Rate").foregroundColor(.secondary)
-                            Text(
-                                "\(rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") U/hr"
-                            )
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
+                        VStack {
+                            HStack {
+                                Text("Rate").foregroundColor(.secondary)
+                                Text(
+                                    "\(rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") U/hr"
+                                )
+                                Spacer()
+                                Text("starts at").foregroundColor(.secondary)
+                                Text(
+                                    "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
+                                )
+                            }
+                            if let basalProfile = state.autotuneProfile[index] {
+                                HStack {
+                                    Text("Autotune")
+                                    Text(rateFormatter.string(from: basalProfile.rate as NSNumber) ?? "0")
+                                    Text("U/hr").foregroundColor(.secondary)
+                                    Spacer()
+                                    Text("@")
+                                    Text(basalProfile.displayTime)
+                                }.font(Font.system(size: 12))
+                            }
                         }
                     }
                     .moveDisabled(true)