浏览代码

testing watch comms

Ivan Valkou 4 年之前
父节点
当前提交
e0a0f8aa6a

+ 11 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -197,6 +197,7 @@
 		38E8754C2755548F00975559 /* WatchStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8754B2755548F00975559 /* WatchStateModel.swift */; };
 		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 */; };
 		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 */; };
@@ -578,6 +579,8 @@
 		38E8754B2755548F00975559 /* WatchStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStateModel.swift; sourceTree = "<group>"; };
 		38E8754E275556FA00975559 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = "<group>"; };
 		38E8755027555D0500975559 /* DataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFlow.swift; sourceTree = "<group>"; };
+		38E8755527564B5000975559 /* FreeAPSWatch WatchKit Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "FreeAPSWatch WatchKit Extension.entitlements"; sourceTree = "<group>"; };
+		38E8755627564B6100975559 /* FreeAPSWatch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FreeAPSWatch.entitlements; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
 		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
 		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@@ -1325,6 +1328,7 @@
 		38E8751D27554D5500975559 /* FreeAPSWatch */ = {
 			isa = PBXGroup;
 			children = (
+				38E8755627564B6100975559 /* FreeAPSWatch.entitlements */,
 				38E8751E27554D5700975559 /* Assets.xcassets */,
 			);
 			path = FreeAPSWatch;
@@ -1333,6 +1337,8 @@
 		38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */ = {
 			isa = PBXGroup;
 			children = (
+				38E8755527564B5000975559 /* FreeAPSWatch WatchKit Extension.entitlements */,
+				38E8755027555D0500975559 /* DataFlow.swift */,
 				38E875482755505800975559 /* Views */,
 				38E8752927554D5700975559 /* FreeAPSApp.swift */,
 				38E8752D27554D5700975559 /* NotificationController.swift */,
@@ -1342,7 +1348,6 @@
 				38E8753827554D5900975559 /* Info.plist */,
 				38E8753927554D5900975559 /* PushNotificationPayload.apns */,
 				38E8753527554D5800975559 /* Preview Content */,
-				38E8755027555D0500975559 /* DataFlow.swift */,
 			);
 			path = "FreeAPSWatch WatchKit Extension";
 			sourceTree = "<group>";
@@ -2138,6 +2143,7 @@
 				DD399FB31EACB9343C944C4C /* PreferencesEditorStateModel.swift in Sources */,
 				44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */,
 				E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */,
+				38E8755427561E9800975559 /* DataFlow.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */,
 				33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */,
@@ -2492,6 +2498,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 0.2.4;
 				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
@@ -2519,6 +2526,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 0.2.4;
 				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
@@ -2544,6 +2552,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = "FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 0.2.4;
 				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";
@@ -2576,6 +2585,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
 				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = "FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements";
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 0.2.4;
 				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";

+ 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>114</integer>
+			<integer>120</integer>
 		</dict>
 		<key>FreeAPSWatch (Notification).xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>113</integer>
+			<integer>119</integer>
 		</dict>
 		<key>FreeAPSWatch.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>112</integer>
+			<integer>118</integer>
 		</dict>
 		<key>ReactiveSwift (Playground) 1.xcscheme</key>
 		<dict>

+ 1 - 1
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -15,6 +15,6 @@ final class ServiceAssembly: Assembly {
         }
         container.register(CalendarManager.self) { r in BaseCalendarManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
-        container.register(WatchManager.self) { _ in BaseWatchManager() }
+        container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
     }
 }

+ 7 - 0
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -4,6 +4,7 @@ import Swinject
 protocol SettingsManager: AnyObject {
     var settings: FreeAPSSettings { get set }
     var preferences: Preferences { get }
+    var pumpSettings: PumpSettings { get }
 }
 
 protocol SettingsObserver {
@@ -45,4 +46,10 @@ final class BaseSettingsManager: SettingsManager, Injectable {
             ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
             ?? Preferences()
     }
+
+    var 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)
+    }
 }

+ 185 - 7
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -1,19 +1,130 @@
 import Foundation
+import Swinject
 import WatchConnectivity
 
 protocol WatchManager {}
 
-final class BaseWatchManager: NSObject, WatchManager {
-    var session: WCSession
+final class BaseWatchManager: NSObject, WatchManager, Injectable {
+    private let session: WCSession
+    private var state = WatchState()
+    private let processQueue = DispatchQueue(label: "BaseWatchManager.processQueue")
 
-    init(session: WCSession = .default) {
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var apsManager: APSManager!
+    @Injected() private var storage: FileStorage!
+
+    init(resolver: Resolver, session: WCSession = .default) {
         self.session = session
         super.init()
+        injectServices(resolver)
 
         if WCSession.isSupported() {
             session.delegate = self
             session.activate()
         }
+
+        broadcaster.register(GlucoseObserver.self, observer: self)
+        broadcaster.register(SuggestionObserver.self, observer: self)
+        broadcaster.register(SettingsObserver.self, observer: self)
+        broadcaster.register(PumpHistoryObserver.self, observer: self)
+        broadcaster.register(PumpSettingsObserver.self, observer: self)
+        broadcaster.register(BasalProfileObserver.self, observer: self)
+        broadcaster.register(TempTargetsObserver.self, observer: self)
+        broadcaster.register(CarbsObserver.self, observer: self)
+        broadcaster.register(EnactedSuggestionObserver.self, observer: self)
+        broadcaster.register(PumpBatteryObserver.self, observer: self)
+        broadcaster.register(PumpReservoirObserver.self, observer: self)
+
+        configureState()
+    }
+
+    private func configureState() {
+        processQueue.async {
+            let glucoseValues = self.glucoseText()
+            self.state.glucose = glucoseValues.glucose
+            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.bolusIncrement = self.settingsManager.preferences.bolusIncrement
+            self.state.maxCOB = self.settingsManager.preferences.maxCOB
+            self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
+            self.state.carbsRequired = self.suggestion?.carbsReq
+
+            let inslinRequired = self.suggestion?.insulinReq ?? 0
+            self.state.bolusRecommended = self.apsManager
+                .roundBolus(amount: max(inslinRequired * self.settingsManager.settings.insulinReqFraction, 0))
+
+            self.state.iob = self.suggestion?.iob
+            self.state.cob = self.suggestion?.cob
+        }
+    }
+
+    private func sendState() {
+        dispatchPrecondition(condition: .onQueue(processQueue))
+        guard let data = try? JSONEncoder().encode(state) else {
+            warning(.service, "Cannot encode watch state")
+            return
+        }
+        guard session.isReachable else {
+            warning(.service, "WCSession is not reachable")
+            return
+        }
+        session.sendMessageData(data, replyHandler: nil) { error in
+            warning(.service, "Cannot send message to watch", error: error)
+        }
+    }
+
+    private func glucoseText() -> (glucose: String, trend: String, delta: String) {
+        let glucose = glucoseStorage.recent()
+
+        guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return ("--", "--", "--") }
+
+        let delta = glucose.count >= 2 ? glucoseValue - (glucose[glucose.count - 2].glucose ?? 0) : nil
+
+        let units = settingsManager.settings.units
+        let glucoseText = glucoseFormatter
+            .string(from: Double(
+                units == .mmolL ? glucoseValue
+                    .asMmolL : Decimal(glucoseValue)
+            ) as NSNumber)! + " " + NSLocalizedString(units.rawValue, comment: "units")
+        let directionText = lastGlucose.direction?.symbol ?? "↔︎"
+        let deltaText = delta
+            .map {
+                self.deltaFormatter
+                    .string(from: Double(
+                        units == .mmolL ? $0
+                            .asMmolL : Decimal($0)
+                    ) as NSNumber)!
+            } ?? "--"
+
+        return (glucoseText, directionText, deltaText)
+    }
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+
+    private var suggestion: Suggestion? {
+        storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
     }
 }
 
@@ -23,12 +134,79 @@ extension BaseWatchManager: WCSessionDelegate {
     func sessionDidDeactivate(_: WCSession) {}
 
     func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
-        print("ASDF state \(state.rawValue)")
+        debug(.service, "WCSession is activated: \(state == .activated)")
     }
 
-    func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
-        print("ASDF message \(message)")
-        session.sendMessage(["message": "It works!"], replyHandler: nil) { _ in
+    func session(_ session: WCSession, didReceiveMessage _: [String: Any]) {
+        session.sendMessage(["message": "It works!"], replyHandler: nil) { _ in }
+    }
+
+    func session(_: WCSession, didReceiveMessageData _: Data) {}
+
+    func sessionReachabilityDidChange(_ session: WCSession) {
+        print("WCSession reacanility: \(session.isReachable)")
+        if session.isReachable {
+            processQueue.async {
+                self.sendState()
+            }
         }
     }
 }
+
+extension BaseWatchManager:
+    GlucoseObserver,
+    SuggestionObserver,
+    SettingsObserver,
+    PumpHistoryObserver,
+    PumpSettingsObserver,
+    BasalProfileObserver,
+    TempTargetsObserver,
+    CarbsObserver,
+    EnactedSuggestionObserver,
+    PumpBatteryObserver,
+    PumpReservoirObserver
+{
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        configureState()
+    }
+
+    func suggestionDidUpdate(_: Suggestion) {
+        configureState()
+    }
+
+    func settingsDidChange(_: FreeAPSSettings) {
+        configureState()
+    }
+
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        // TODO:
+    }
+
+    func pumpSettingsDidChange(_: PumpSettings) {
+        configureState()
+    }
+
+    func basalProfileDidChange(_: [BasalProfileEntry]) {
+        // TODO:
+    }
+
+    func tempTargetsDidUpdate(_: [TempTarget]) {
+        configureState()
+    }
+
+    func carbsDidUpdate(_: [CarbsEntry]) {
+        // TODO:
+    }
+
+    func enactedSuggestionDidUpdate(_: Suggestion) {
+        // TODO:
+    }
+
+    func pumpBatteryDidChange(_: Battery) {
+        // TODO:
+    }
+
+    func pumpReservoirDidChange(_: Decimal) {
+        // TODO:
+    }
+}

+ 32 - 1
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -1,3 +1,34 @@
 import Foundation
 
-struct Message {}
+enum WatchMessageType: String {
+    case state
+    case confirmation
+    case command
+}
+
+struct WatchMessage {
+    let type: WatchMessageType
+    let carbs: Int?
+    let tempTargetID: String?
+    let bolusUnits: Decimal?
+}
+
+struct WatchState: Codable {
+    var glucose: String?
+    var trend: String?
+    var delta: String?
+    var glucoseDate: Date?
+    var lastLoopDate: Date?
+    var bolusIncrement: Decimal?
+    var maxCOB: Decimal?
+    var maxBolus: Decimal?
+    var carbsRequired: Decimal?
+    var bolusRecommended: Decimal?
+    var iob: Decimal?
+    var cob: Decimal?
+}
+
+struct WatchCommandConfitmation: Codable {
+    let confirmed: Bool
+    let reason: String?
+}

+ 10 - 0
FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>group.com.BA7ZHP4963.loopkit.LoopGroup</string>
+	</array>
+</dict>
+</plist>

+ 47 - 36
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -7,47 +7,58 @@ struct MainView: View {
     @State var isTargetsActive = false
     @State var isBolusActive = false
     var body: some View {
-        Form {
-            NavigationLink {
-                CarbsView()
-                    .environmentObject(state)
-            } label: {
-                HStack {
-                    Image("carbs", bundle: nil)
-                        .renderingMode(.template)
-                        .resizable()
-                        .frame(width: 24, height: 24)
-                        .foregroundColor(.loopGreen)
-                    Text("Abb Carbs")
+        VStack {
+            header
+            Form {
+                NavigationLink {
+                    CarbsView()
+                        .environmentObject(state)
+                } label: {
+                    HStack {
+                        Image("carbs", bundle: nil)
+                            .renderingMode(.template)
+                            .resizable()
+                            .frame(width: 24, height: 24)
+                            .foregroundColor(.loopGreen)
+                        Text("Abb Carbs")
+                    }
                 }
-            }
 
-            NavigationLink {
-                EmptyView()
-            } label: {
-                HStack {
-                    Image("target", bundle: nil)
-                        .renderingMode(.template)
-                        .resizable()
-                        .frame(width: 24, height: 24)
-                        .foregroundColor(.loopYellow)
-                    Text("Temp Targets").foregroundColor(.primary)
+                NavigationLink {
+                    EmptyView()
+                } label: {
+                    HStack {
+                        Image("target", bundle: nil)
+                            .renderingMode(.template)
+                            .resizable()
+                            .frame(width: 24, height: 24)
+                            .foregroundColor(.loopYellow)
+                        Text("Temp Targets").foregroundColor(.primary)
+                    }
                 }
-            }
 
-            NavigationLink {
-                EmptyView()
-            } label: {
-                HStack {
-                    Image("bolus", bundle: nil)
-                        .renderingMode(.template)
-                        .resizable()
-                        .frame(width: 24, height: 24)
-                        .foregroundColor(.insulin)
-                    Text("Enact Bolus")
+                NavigationLink {
+                    EmptyView()
+                } label: {
+                    HStack {
+                        Image("bolus", bundle: nil)
+                            .renderingMode(.template)
+                            .resizable()
+                            .frame(width: 24, height: 24)
+                            .foregroundColor(.insulin)
+                        Text("Enact Bolus")
+                    }
                 }
-            }
-        }.padding()
+            }.padding()
+        }
+    }
+
+    var header: some View {
+        HStack {
+            Text(state.glucose).font(.title2)
+            Text(state.trend)
+            Text(state.delta)
+        }
     }
 }
 

+ 30 - 1
FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift

@@ -6,6 +6,17 @@ class WatchStateModel: NSObject, ObservableObject {
     var session: WCSession
     @Published var result = ""
 
+    @Published var glucose = "0"
+    @Published var trend = "??"
+    @Published var delta = "??"
+    @Published var lastLoopDate: Date?
+    @Published var bolusIncrement: Decimal?
+    @Published var maxCOB: Int?
+    @Published var maxBolus: Decimal?
+    @Published var bolusRecommended: Decimal?
+    @Published var iob: Decimal?
+    @Published var cob: Decimal?
+
     init(session: WCSession = .default) {
         self.session = session
         super.init()
@@ -19,11 +30,21 @@ class WatchStateModel: NSObject, ObservableObject {
             print("ASDF: " + error.localizedDescription)
         }
     }
+
+    private func processState(_ state: WatchState) {
+        glucose = state.glucose ?? "?"
+        trend = state.trend ?? "?"
+        delta = state.delta ?? "?"
+    }
 }
 
 extension WatchStateModel: WCSessionDelegate {
     func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
-        print("ASDF state \(state.rawValue)")
+        print("WCSession activated: \(state == .activated)")
+
+        session.sendMessage(["active": true], replyHandler: nil) { error in
+            print("ASDF: " + error.localizedDescription)
+        }
     }
 
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
@@ -33,4 +54,12 @@ extension WatchStateModel: WCSessionDelegate {
             }
         }
     }
+
+    func session(_: WCSession, didReceiveMessageData messageData: Data) {
+        if let state = try? JSONDecoder().decode(WatchState.self, from: messageData) {
+            DispatchQueue.main.async {
+                self.processState(state)
+            }
+        }
+    }
 }

+ 10 - 0
FreeAPSWatch/FreeAPSWatch.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>group.com.BA7ZHP4963.loopkit.LoopGroup</string>
+	</array>
+</dict>
+</plist>