Ivan Valkou 4 лет назад
Родитель
Сommit
e28d845299

+ 12 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -198,6 +198,8 @@
 		38E8754F275556FA00975559 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8754E275556FA00975559 /* WatchManager.swift */; };
 		38E8755127555D0500975559 /* DataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8755027555D0500975559 /* DataFlow.swift */; };
 		38E8755427561E9800975559 /* DataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8755027555D0500975559 /* DataFlow.swift */; };
+		38E8755827567AE400975559 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 38E8755727567AE400975559 /* SwiftDate */; };
+		38E8755927567CA600975559 /* Decimal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
 		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
 		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
@@ -713,6 +715,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				38E8755827567AE400975559 /* SwiftDate in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -1810,6 +1813,9 @@
 			dependencies = (
 			);
 			name = "FreeAPSWatch WatchKit Extension";
+			packageProductDependencies = (
+				38E8755727567AE400975559 /* SwiftDate */,
+			);
 			productName = "FreeAPSWatch WatchKit Extension";
 			productReference = 38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */;
 			productType = "com.apple.product-type.watchkit2-extension";
@@ -2212,6 +2218,7 @@
 				38E8752A27554D5700975559 /* FreeAPSApp.swift in Sources */,
 				38E8753027554D5700975559 /* NotificationView.swift in Sources */,
 				38E8754727554DF100975559 /* Color+Extensions.swift in Sources */,
+				38E8755927567CA600975559 /* Decimal+Extensions.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -2752,6 +2759,11 @@
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
 			productName = SwiftDate;
 		};
+		38E8755727567AE400975559 /* SwiftDate */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
+			productName = SwiftDate;
+		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 388E595025AD948C0019842D /* Project object */;

+ 3 - 3
FreeAPS.xcodeproj/xcuserdata/i.valkou.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -33,17 +33,17 @@
 		<key>FreeAPSWatch (Complication).xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>120</integer>
+			<integer>132</integer>
 		</dict>
 		<key>FreeAPSWatch (Notification).xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>119</integer>
+			<integer>131</integer>
 		</dict>
 		<key>FreeAPSWatch.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>118</integer>
+			<integer>130</integer>
 		</dict>
 		<key>ReactiveSwift (Playground) 1.xcscheme</key>
 		<dict>

+ 24 - 6
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -47,7 +47,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             self.state.trend = glucoseValues.trend
             self.state.delta = glucoseValues.delta
             self.state.glucoseDate = self.glucoseStorage.recent().last?.dateString
-            self.state.lastLoopDate = self.apsManager.lastLoopDate
+            self.state.lastLoopDate = self.enactedSuggestion?.deliverAt
             self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
             self.state.maxCOB = self.settingsManager.preferences.maxCOB
             self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
@@ -59,6 +59,8 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
 
             self.state.iob = self.suggestion?.iob
             self.state.cob = self.suggestion?.cob
+
+            self.sendState()
         }
     }
 
@@ -89,7 +91,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
             .string(from: Double(
                 units == .mmolL ? glucoseValue
                     .asMmolL : Decimal(glucoseValue)
-            ) as NSNumber)! + " " + NSLocalizedString(units.rawValue, comment: "units")
+            ) as NSNumber)!
         let directionText = lastGlucose.direction?.symbol ?? "↔︎"
         let deltaText = delta
             .map {
@@ -126,6 +128,10 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     private var suggestion: Suggestion? {
         storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
     }
+
+    private var enactedSuggestion: Suggestion? {
+        storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self)
+    }
 }
 
 extension BaseWatchManager: WCSessionDelegate {
@@ -137,14 +143,26 @@ extension BaseWatchManager: WCSessionDelegate {
         debug(.service, "WCSession is activated: \(state == .activated)")
     }
 
-    func session(_ session: WCSession, didReceiveMessage _: [String: Any]) {
-        session.sendMessage(["message": "It works!"], replyHandler: nil) { _ in }
+    func session(_: WCSession, didReceiveMessage message: [String: Any]) {
+        guard let commandRaw = message[WatchCommandKey.command.rawValue] as? String,
+              let command = WatchCommand(rawValue: commandRaw) else { return }
+        switch command {
+        case .stateRequest:
+            processQueue.async {
+                self.sendState()
+            }
+        case .carbs: break
+        }
+    }
+
+    func session(_: WCSession, didReceiveMessage _: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
+        print("WCSession got message with reply handler")
+        replyHandler(["message": "ok"])
     }
 
     func session(_: WCSession, didReceiveMessageData _: Data) {}
 
     func sessionReachabilityDidChange(_ session: WCSession) {
-        print("WCSession reacanility: \(session.isReachable)")
         if session.isReachable {
             processQueue.async {
                 self.sendState()
@@ -199,7 +217,7 @@ extension BaseWatchManager:
     }
 
     func enactedSuggestionDidUpdate(_: Suggestion) {
-        // TODO:
+        configureState()
     }
 
     func pumpBatteryDidChange(_: Battery) {

+ 4 - 8
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -1,16 +1,12 @@
 import Foundation
 
-enum WatchMessageType: String {
-    case state
-    case confirmation
+enum WatchCommandKey: String {
     case command
 }
 
-struct WatchMessage {
-    let type: WatchMessageType
-    let carbs: Int?
-    let tempTargetID: String?
-    let bolusUnits: Decimal?
+enum WatchCommand: String {
+    case stateRequest
+    case carbs
 }
 
 struct WatchState: Codable {

+ 3 - 1
FreeAPSWatch WatchKit Extension/FreeAPSApp.swift

@@ -1,11 +1,13 @@
 import SwiftUI
 
 @main struct FreeAPSApp: App {
+    @StateObject var state = WatchStateModel()
+
     @SceneBuilder var body: some Scene {
         WindowGroup {
             NavigationView {
                 MainView()
-            }
+            }.environmentObject(state)
         }
 
         WKNotificationScene(controller: NotificationController.self, category: "myCategory")

+ 5 - 2
FreeAPSWatch WatchKit Extension/Views/CarbsView.swift

@@ -9,7 +9,7 @@ struct CarbsView: View {
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.minimum = 0
-        formatter.maximum = 120
+        formatter.maximum = (state.maxCOB ?? 120) as NSNumber
         formatter.maximumFractionDigits = 0
         formatter.allowsFloats = false
         return formatter
@@ -39,7 +39,7 @@ struct CarbsView: View {
                 Spacer()
                 Button {
                     let newValue = amount + 5
-                    amount = min(newValue, 120)
+                    amount = min(newValue, Double(state.maxCOB ?? 120))
                 } label: { Image(systemName: "plus") }
                     .frame(width: 50)
             }
@@ -59,6 +59,9 @@ struct CarbsView: View {
             .disabled(amount <= 0)
         }
         .navigationTitle("Add Carbs")
+        .onAppear {
+            amount = Double(state.carbsRequired ?? 0)
+        }
     }
 }
 

+ 107 - 39
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -1,7 +1,12 @@
+import SwiftDate
 import SwiftUI
 
 struct MainView: View {
-    @StateObject var state = WatchStateModel()
+    private enum Config {
+        static let lag: TimeInterval = 30
+    }
+
+    @EnvironmentObject var state: WatchStateModel
 
     @State var isCarbsActive = false
     @State var isTargetsActive = false
@@ -9,61 +14,124 @@ struct MainView: View {
     var body: some View {
         VStack {
             header
-            Form {
-                NavigationLink {
-                    CarbsView()
-                        .environmentObject(state)
-                } label: {
+            Spacer()
+            buttons
+        }
+        .frame(maxHeight: .infinity)
+        .padding()
+    }
+
+    var header: some View {
+        VStack {
+            HStack(alignment: .top) {
+                VStack(alignment: .leading) {
                     HStack {
-                        Image("carbs", bundle: nil)
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 24, height: 24)
-                            .foregroundColor(.loopGreen)
-                        Text("Abb Carbs")
+                        Text(state.glucose).font(.largeTitle)
+                        Text(state.trend)
                     }
+                    Text(state.delta).font(.caption2)
                 }
+                Spacer()
 
-                NavigationLink {
-                    EmptyView()
-                } label: {
+                VStack(spacing: 0) {
                     HStack {
-                        Image("target", bundle: nil)
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 24, height: 24)
-                            .foregroundColor(.loopYellow)
-                        Text("Temp Targets").foregroundColor(.primary)
+                        Circle().stroke(color, lineWidth: 6).frame(width: 30, height: 30).padding(10)
                     }
-                }
 
-                NavigationLink {
-                    EmptyView()
-                } label: {
-                    HStack {
-                        Image("bolus", bundle: nil)
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 24, height: 24)
-                            .foregroundColor(.insulin)
-                        Text("Enact Bolus")
+                    if state.lastLoopDate != nil {
+                        Text(timeString).font(.caption2)
+                    } else {
+                        Text("--").font(.caption2)
                     }
                 }
-            }.padding()
-        }
+            }
+            Spacer()
+            HStack {
+                Text("IOB: " + iobFormatter.string(from: (state.iob ?? 0) as NSNumber)! + " U").font(.caption2)
+                Spacer()
+                Text("COB: " + iobFormatter.string(from: (state.cob ?? 0) as NSNumber)! + " g").font(.caption2)
+            }
+            Spacer()
+        }.padding()
     }
 
-    var header: some View {
+    var buttons: some View {
         HStack {
-            Text(state.glucose).font(.title2)
-            Text(state.trend)
-            Text(state.delta)
+            NavigationLink {
+                CarbsView()
+                    .environmentObject(state)
+            } label: {
+                HStack {
+                    Image("carbs", bundle: nil)
+                        .renderingMode(.template)
+                        .resizable()
+                        .frame(width: 24, height: 24)
+                        .foregroundColor(.loopGreen)
+                }
+            }
+
+            NavigationLink {
+                EmptyView()
+            } label: {
+                HStack {
+                    Image("target", bundle: nil)
+                        .renderingMode(.template)
+                        .resizable()
+                        .frame(width: 24, height: 24)
+                        .foregroundColor(.loopYellow)
+                }
+            }
+
+            NavigationLink {
+                EmptyView()
+            } label: {
+                HStack {
+                    Image("bolus", bundle: nil)
+                        .renderingMode(.template)
+                        .resizable()
+                        .frame(width: 24, height: 24)
+                        .foregroundColor(.insulin)
+                }
+            }
+        }
+    }
+
+    private var iobFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.maximumFractionDigits = 2
+        formatter.numberStyle = .decimal
+        return formatter
+    }
+
+    private var timeString: String {
+        let minAgo = Int((Date().timeIntervalSince(state.lastLoopDate ?? .distantPast) - Config.lag) / 60) + 1
+        if minAgo > 1440 {
+            return "--"
+        }
+        return "\(minAgo) " + NSLocalizedString("min ago", comment: "Minutes ago since last loop")
+    }
+
+    private var color: Color {
+        guard let lastLoopDate = state.lastLoopDate else {
+            return .loopGray
+        }
+        let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag
+
+        if delta <= 5.minutes.timeInterval {
+            return .loopGreen
+        } else if delta <= 10.minutes.timeInterval {
+            return .loopYellow
+        } else {
+            return .loopRed
         }
     }
 }
 
 struct ContentView_Previews: PreviewProvider {
     static var previews: some View {
-        MainView()
+        Group {
+            MainView().environmentObject(WatchStateModel())
+            MainView().previewDevice("Apple Watch Series 5 - 40mm").environmentObject(WatchStateModel())
+        }
     }
 }

+ 24 - 8
FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift

@@ -6,14 +6,16 @@ class WatchStateModel: NSObject, ObservableObject {
     var session: WCSession
     @Published var result = ""
 
-    @Published var glucose = "0"
-    @Published var trend = "??"
-    @Published var delta = "??"
+    @Published var glucose = "00"
+    @Published var trend = ""
+    @Published var delta = "+00"
     @Published var lastLoopDate: Date?
+    @Published var glucoseDate: Date?
     @Published var bolusIncrement: Decimal?
-    @Published var maxCOB: Int?
+    @Published var maxCOB: Decimal?
     @Published var maxBolus: Decimal?
     @Published var bolusRecommended: Decimal?
+    @Published var carbsRequired: Decimal?
     @Published var iob: Decimal?
     @Published var cob: Decimal?
 
@@ -26,7 +28,9 @@ class WatchStateModel: NSObject, ObservableObject {
     }
 
     func addCarbs(_ carbs: Int) {
-        session.sendMessage(["addCarbs": carbs], replyHandler: nil) { error in
+        session.sendMessage(["addCarbs": carbs], replyHandler: { _ in
+            WKInterfaceDevice.current().play(.success)
+        }) { error in
             print("ASDF: " + error.localizedDescription)
         }
     }
@@ -35,15 +39,22 @@ class WatchStateModel: NSObject, ObservableObject {
         glucose = state.glucose ?? "?"
         trend = state.trend ?? "?"
         delta = state.delta ?? "?"
+        glucoseDate = state.glucoseDate
+        lastLoopDate = state.lastLoopDate
+        bolusIncrement = state.bolusIncrement
+        maxCOB = state.maxCOB
+        bolusRecommended = state.bolusRecommended
+        carbsRequired = state.carbsRequired
+        iob = state.iob
+        cob = state.cob
     }
 }
 
 extension WatchStateModel: WCSessionDelegate {
     func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
         print("WCSession activated: \(state == .activated)")
-
-        session.sendMessage(["active": true], replyHandler: nil) { error in
-            print("ASDF: " + error.localizedDescription)
+        session.sendMessage([WatchCommandKey.command.rawValue: WatchCommand.stateRequest.rawValue], replyHandler: nil) { error in
+            print("WatchStateModel error: " + error.localizedDescription)
         }
     }
 
@@ -55,9 +66,14 @@ extension WatchStateModel: WCSessionDelegate {
         }
     }
 
+    func sessionReachabilityDidChange(_ session: WCSession) {
+        print("WCSession Reachability: \(session.isReachable)")
+    }
+
     func session(_: WCSession, didReceiveMessageData messageData: Data) {
         if let state = try? JSONDecoder().decode(WatchState.self, from: messageData) {
             DispatchQueue.main.async {
+//                WKInterfaceDevice.current().play(.click)
                 self.processState(state)
             }
         }