Przeglądaj źródła

Closed loop toggle

Ivan Valkou 5 lat temu
rodzic
commit
0db7675bca

+ 45 - 26
FreeAPS/Sources/APS/APSManager.swift

@@ -5,8 +5,7 @@ import LoopKitUI
 import Swinject
 
 protocol APSManager {
-    func determineBasal()
-    func fetchLastGlucose()
+    func loop()
     func autosense()
     func autotune()
     var pumpManager: PumpManagerUI? { get set }
@@ -18,12 +17,13 @@ final class BaseAPSManager: APSManager, Injectable {
     @Injected() private var pumpHistoryStorage: PumpHistoryStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var tempTargetsStorage: TempTargetsStorage!
-    @Injected() private var keychain: Keychain!
     @Injected() private var deviceDataManager: DeviceDataManager!
+    @Injected() private var networkManager: NetworkManager!
+    @Injected() private var settingsManager: SettingsManager!
     private var openAPS: OpenAPS!
 
-    private var glucoseCancellable: AnyCancellable?
-    private var determineBasalCancellable: AnyCancellable?
+    private var loopCancellable: AnyCancellable?
+    private var pumpCancellable: AnyCancellable?
     private var enactCancellable: AnyCancellable?
 
     var pumpManager: PumpManagerUI? {
@@ -35,40 +35,57 @@ final class BaseAPSManager: APSManager, Injectable {
         deviceDataManager.pumpDisplayState
     }
 
+    var settings: FreeAPSSettings {
+        get { settingsManager.settings }
+        set { settingsManager.settings = newValue }
+    }
+
     init(resolver: Resolver) {
         injectServices(resolver)
         openAPS = OpenAPS(storage: storage)
+        subscribe()
+    }
+
+    private func subscribe() {
+        pumpCancellable = deviceDataManager.recommendsLoop
+            .sink { [weak self] in
+                self?.loop()
+            }
     }
 
-    func loop() {}
+    func loop() {
+        loopCancellable = networkManager
+            .fetchGlucose()
+            .flatMap { [weak self] glucose -> AnyPublisher<Bool, Never> in
+                guard let self = self else { return Just(false).eraseToAnyPublisher() }
+                self.glucoseStorage.storeGlucose(glucose)
+                return self.determineBasal()
+            }
+            .sink { _ in } receiveValue: { [weak self] ok in
+                guard let self = self else { return }
+                if ok, self.settings.closedLoop {
+                    self.enactSuggested()
+                }
+            }
+    }
 
-    func determineBasal() {
+    func determineBasal() -> AnyPublisher<Bool, Never> {
         guard let glucose = try? storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self), glucose.count >= 36 else {
             print("Not enough glucose data")
-            return
+            return Just(false).eraseToAnyPublisher()
         }
 
         let now = Date()
-        guard let temp = currentTemp(date: now) else { return }
-        determineBasalCancellable = openAPS.makeProfiles()
+        guard let temp = currentTemp(date: now) else {
+            return Just(false).eraseToAnyPublisher()
+        }
+
+        return openAPS.makeProfiles()
             .flatMap { _ in
                 self.openAPS.determineBasal(currentTemp: temp, clock: now)
             }
-            .sink { [weak self] in
-                self?.enactSuggested()
-            }
-    }
-
-    func fetchLastGlucose() {
-        if let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
-           let url = URL(string: urlString)
-        {
-            glucoseCancellable = NightscoutAPI(url: url).fetchLast(288)
-                .sink { _ in }
-            receiveValue: { glucose in
-                self.glucoseStorage.storeGlucose(glucose)
-            }
-        }
+            .map { true }
+            .eraseToAnyPublisher()
     }
 
     func autosense() {
@@ -128,7 +145,9 @@ final class BaseAPSManager: APSManager, Injectable {
                 }
             } receiveValue: { [weak self] in
                 print("Loop succeeded")
-                try? self?.storage.save(suggested, as: OpenAPS.Enact.enacted)
+                if let rawSuggested = self?.storage.retrieveRaw(OpenAPS.Enact.suggested) {
+                    try? self?.storage.save(rawSuggested, as: OpenAPS.Enact.enacted)
+                }
             }
     }
 }

+ 4 - 0
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -12,6 +12,7 @@ import UserNotifications
 protocol DeviceDataManager {
     var pumpManager: PumpManagerUI? { get set }
     var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
+    var recommendsLoop: PassthroughSubject<Void, Never> { get }
 }
 
 private let staticPumpManagers: [PumpManagerUI.Type] = [
@@ -30,6 +31,8 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
 
     @Persisted(key: "BaseDeviceDataManager.lastEventDate") var lastEventDate: Date? = nil
 
+    let recommendsLoop = PassthroughSubject<Void, Never>()
+
     var pumpManager: PumpManagerUI? {
         didSet {
             pumpManager?.pumpManagerDelegate = self
@@ -143,6 +146,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
     func pumpManagerRecommendsLoop(_: PumpManager) {
         print("[DeviceDataManager] Recomends loop")
+        recommendsLoop.send()
     }
 
     func startDateToFilterNewPumpEvents(for _: PumpManager) -> Date {

+ 8 - 8
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -12,7 +12,7 @@ final class OpenAPS {
         self.storage = storage
     }
 
-    func determineBasal(currentTemp: TempBasal, clock: Date = Date()) -> AnyPublisher<Void, Never> {
+    func determineBasal(currentTemp: TempBasal, clock: Date = Date()) -> Future<Void, Never> {
         Future { promise in
             self.processQueue.async {
                 // clock
@@ -70,10 +70,10 @@ final class OpenAPS {
 
                 promise(.success(()))
             }
-        }.eraseToAnyPublisher()
+        }
     }
 
-    func autosense() -> AnyPublisher<Void, Never> {
+    func autosense() -> Future<Void, Never> {
         Future { promise in
             self.processQueue.async {
                 let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
@@ -95,10 +95,10 @@ final class OpenAPS {
                 try? self.storage.save(autosensResult, as: Settings.autosense)
                 promise(.success(()))
             }
-        }.eraseToAnyPublisher()
+        }
     }
 
-    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> AnyPublisher<Void, Never> {
+    func autotune(categorizeUamAsBasal: Bool = false, tuneInsulinCurve: Bool = false) -> Future<Void, Never> {
         Future { promise in
             self.processQueue.async {
                 let pumpHistory = self.loadFileFromStorage(name: OpenAPS.Monitor.pumpHistory)
@@ -128,10 +128,10 @@ final class OpenAPS {
                 print("AUTOTUNE RESULT: \(autotuneResult)")
                 promise(.success(()))
             }
-        }.eraseToAnyPublisher()
+        }
     }
 
-    func makeProfiles() -> AnyPublisher<Void, Never> {
+    func makeProfiles() -> Future<Void, Never> {
         Future { promise in
             self.processQueue.async {
                 let preferences = self.loadFileFromStorage(name: Settings.preferences)
@@ -173,7 +173,7 @@ final class OpenAPS {
 
                 promise(.success(()))
             }
-        }.eraseToAnyPublisher()
+        }
     }
 
     // MARK: - Private

+ 1 - 5
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -6,16 +6,12 @@ extension Home {
         @Injected() var history: PumpHistoryStorage!
         @Injected() var temps: TempTargetsStorage!
 
-        func fetchGlucose() {
-            apsManager.fetchLastGlucose()
-        }
-
         func addCarbs() {
             history.storeJournalCarbs(15)
         }
 
         func runLoop() {
-            apsManager.determineBasal()
+            apsManager.loop()
         }
 
         func addHighTempTarget() {

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

@@ -7,12 +7,6 @@ extension Home {
         var body: some View {
             VStack {
                 Spacer()
-                Button(action: viewModel.fetchGlucose) {
-                    Text("Fetch glucose")
-                        .frame(maxWidth: .infinity)
-                        .foregroundColor(.white)
-                        .buttonBackground()
-                }
                 Button(action: viewModel.addCarbs) {
                     Text("Add 15 g carbs")
                         .frame(maxWidth: .infinity)

+ 14 - 1
FreeAPS/Sources/Modules/Settings/SettingsViewModel.swift

@@ -1,5 +1,18 @@
 import SwiftUI
 
 extension Settings {
-    class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: SettingsProvider {}
+    class ViewModel<Provider>: BaseViewModel<Provider>, ObservableObject where Provider: SettingsProvider {
+        @Injected() private var settingsManager: SettingsManager!
+        @Published var closedLoop = false
+
+        override func subscribe() {
+            closedLoop = settingsManager.settings.closedLoop
+
+            $closedLoop
+                .removeDuplicates()
+                .sink { [weak self] value in
+                    self?.settingsManager.settings.closedLoop = value
+                }.store(in: &lifetime)
+        }
+    }
 }

+ 6 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -6,6 +6,10 @@ extension Settings {
 
         var body: some View {
             Form {
+                Section(header: Text("FreeAPS X")) {
+                    Toggle("Closed loop", isOn: $viewModel.closedLoop)
+                }
+
                 Section(header: Text("Devices")) {
                     Text("Pump").chevronCell().modal(for: .pumpConfig, from: self)
                 }
@@ -48,6 +52,8 @@ extension Settings {
                         Text("Glucose").chevronCell().modal(for: .configEditor(file: OpenAPS.Monitor.glucose), from: self)
                         Text("Suggested").chevronCell()
                             .modal(for: .configEditor(file: OpenAPS.Enact.suggested), from: self)
+                        Text("Enacted").chevronCell()
+                            .modal(for: .configEditor(file: OpenAPS.Enact.enacted), from: self)
                     }
                 }
             }

+ 12 - 1
FreeAPS/Sources/Services/Network/NetworkManager.swift

@@ -2,11 +2,15 @@ import Combine
 import Foundation
 import Swinject
 
-protocol NetworkManager {}
+protocol NetworkManager {
+    func fetchGlucose() -> AnyPublisher<[BloodGlucose], Error>
+}
 
 final class BaseNetworkManager: NetworkManager, Injectable {
     @Injected() private var keychain: Keychain!
 
+    private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
+
     private var nightscoutAPI: NightscoutAPI? {
         guard let urlString = keychain.getValue(String.self, forKey: NightscoutConfig.Config.urlKey),
               let url = URL(string: urlString),
@@ -20,4 +24,11 @@ final class BaseNetworkManager: NetworkManager, Injectable {
     init(resolver: Resolver) {
         injectServices(resolver)
     }
+
+    func fetchGlucose() -> AnyPublisher<[BloodGlucose], Error> {
+        guard let nightscout = nightscoutAPI else {
+            return Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
+        }
+        return nightscout.fetchLast(288)
+    }
 }

+ 2 - 1
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -10,7 +10,7 @@ class NightscoutAPI {
 
     private enum Config {
         static let entriesPath = "/api/v1/entries/sgv.json"
-        static let retryCount = 5
+        static let retryCount = 2
     }
 
     enum Error: LocalizedError {
@@ -54,6 +54,7 @@ extension NightscoutAPI {
 
         var request = URLRequest(url: components.url!)
         request.allowsConstrainedNetworkAccess = false
+        request.timeoutInterval = 10
 
         return URLSession.shared.dataTaskPublisher(for: request)
             .retry(Config.retryCount)