diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 8bdf86109e..ba0cf3ad67 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; 4302F4E51D4EA75100F0FCAF /* DoseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */; }; 43076BF31DFDBC4B0012A723 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43076BF21DFDBC4B0012A723 /* it.lproj */; }; + 4309786C1E73D2F500BEBC82 /* it.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 4309786B1E73D2F500BEBC82 /* it.lproj */; }; + 4309786E1E73DAD100BEBC82 /* CGM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4309786D1E73DAD100BEBC82 /* CGM.swift */; }; 430C1ABD1E5568A80067F1AE /* StatusChartManager+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C1ABC1E5568A80067F1AE /* StatusChartManager+LoopKit.swift */; }; 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58D1D4AEC230097D1CA /* NSBundle.swift */; }; 430DA5901D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */; }; @@ -74,6 +76,9 @@ 438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */; }; 439897371CD2F80600223065 /* AnalyticsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897361CD2F80600223065 /* AnalyticsManager.swift */; }; 4398973B1CD2FC2000223065 /* NSDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */; }; + 439BED2A1E76093C00B0AED5 /* CGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED291E76093C00B0AED5 /* CGMManager.swift */; }; + 439BED2C1E760A7A00B0AED5 /* DexCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED2B1E760A7A00B0AED5 /* DexCGMManager.swift */; }; + 439BED2E1E760BC600B0AED5 /* EnliteCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED2D1E760BC600B0AED5 /* EnliteCGMManager.swift */; }; 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A567681C94880B00334FAC /* LoopDataManager.swift */; }; 43A5676B1C96155700334FAC /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */; }; 43A943761B926B7B0051FA24 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43A943741B926B7B0051FA24 /* Interface.storyboard */; }; @@ -209,10 +214,7 @@ C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */; }; C17884631D51A7A400405663 /* BatteryIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C17884621D51A7A400405663 /* BatteryIndicator.swift */; }; C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */; }; - C1C6591A1E1B1F430025CC58 /* (null) in Sources */ = {isa = PBXBuildFile; }; C1C6591C1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json in Resources */ = {isa = PBXBuildFile; fileRef = C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */; }; - C1C73F021DE3D0250022FC89 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F041DE3D0250022FC89 /* Localizable.strings */; }; - C1C73F081DE3D0260022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F0A1DE3D0260022FC89 /* InfoPlist.strings */; }; C1C73F0D1DE3D0270022FC89 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1C73F0F1DE3D0270022FC89 /* InfoPlist.strings */; }; C9886AE51E5B2FAD00473BB8 /* gallery.ckcomplication in Resources */ = {isa = PBXBuildFile; fileRef = C9886AE41E5B2FAD00473BB8 /* gallery.ckcomplication */; }; /* End PBXBuildFile section */ @@ -335,6 +337,8 @@ 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoseStore.swift; sourceTree = ""; }; 43076BF21DFDBC4B0012A723 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; + 4309786B1E73D2F500BEBC82 /* it.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = it.lproj; sourceTree = ""; }; + 4309786D1E73DAD100BEBC82 /* CGM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGM.swift; sourceTree = ""; }; 430C1ABC1E5568A80067F1AE /* StatusChartManager+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatusChartManager+LoopKit.swift"; sourceTree = ""; }; 430DA58D1D4AEC230097D1CA /* NSBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSBundle.swift; sourceTree = ""; }; 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MySentryPumpStatusMessageBody.swift; sourceTree = ""; }; @@ -407,6 +411,9 @@ 439897341CD2F7DE00223065 /* NSTimeInterval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSTimeInterval.swift; sourceTree = ""; }; 439897361CD2F80600223065 /* AnalyticsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AnalyticsManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateFormatter.swift; sourceTree = ""; }; + 439BED291E76093C00B0AED5 /* CGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMManager.swift; sourceTree = ""; }; + 439BED2B1E760A7A00B0AED5 /* DexCGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DexCGMManager.swift; sourceTree = ""; }; + 439BED2D1E760BC600B0AED5 /* EnliteCGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnliteCGMManager.swift; sourceTree = ""; }; 43A567681C94880B00334FAC /* LoopDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LoopDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; 43A943721B926B7B0051FA24 /* WatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -518,8 +525,6 @@ C17884621D51A7A400405663 /* BatteryIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatteryIndicator.swift; sourceTree = ""; }; C18C8C501D5A351900E043FB /* NightscoutDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutDataManager.swift; sourceTree = ""; }; C1C6591B1E1B1FDA0025CC58 /* recommend_temp_basal_dropping_then_rising.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = recommend_temp_basal_dropping_then_rising.json; sourceTree = ""; }; - C1C73F031DE3D0250022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; - C1C73F091DE3D0260022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; C1C73F0E1DE3D0270022FC89 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; C9886AE41E5B2FAD00473BB8 /* gallery.ckcomplication */ = {isa = PBXFileReference; lastKnownFileType = folder; path = gallery.ckcomplication; sourceTree = ""; }; /* End PBXFileReference section */ @@ -627,10 +632,14 @@ children = ( 43880F961D9D8052009061A8 /* ServiceAuthentication */, 43DE92601C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift */, + C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */, + 4309786D1E73DAD100BEBC82 /* CGM.swift */, 4331E0791C85650D00FBE832 /* ChartAxisValueDoubleLog.swift */, 43F41C321D3A17AA00C11ED6 /* ChartAxisValueDoubleUnit.swift */, + 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */, 43E397A21D56B9E40028E321 /* Glucose.swift */, 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */, + C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */, 436A0DA41D236A2A00104B24 /* LoopError.swift */, 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */, @@ -638,9 +647,6 @@ 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, - C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, - C17824A41E1AD4D100D9D25C /* BolusRecommendation.swift */, - 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */, ); path = Models; sourceTree = ""; @@ -680,12 +686,11 @@ isa = PBXGroup; children = ( C9886AE41E5B2FAD00473BB8 /* gallery.ckcomplication */, + 4309786B1E73D2F500BEBC82 /* it.lproj */, 43EDEE6B1CF2E12A00393BE3 /* Loop.entitlements */, 43F5C2D41B92A4A6003EB13D /* Info.plist */, 43776F8F1B8022E90074EA36 /* AppDelegate.swift */, 43776F981B8022E90074EA36 /* Assets.xcassets */, - C1C73F0A1DE3D0260022FC89 /* InfoPlist.strings */, - C1C73F041DE3D0250022FC89 /* Localizable.strings */, 43776F9A1B8022E90074EA36 /* LaunchScreen.storyboard */, 43776F951B8022E90074EA36 /* Main.storyboard */, 43E344A01B9E144300C85C07 /* Extensions */, @@ -710,6 +715,16 @@ path = ServiceAuthentication; sourceTree = ""; }; + 439BED281E76091600B0AED5 /* CGM */ = { + isa = PBXGroup; + children = ( + 439BED291E76093C00B0AED5 /* CGMManager.swift */, + 439BED2B1E760A7A00B0AED5 /* DexCGMManager.swift */, + 439BED2D1E760BC600B0AED5 /* EnliteCGMManager.swift */, + ); + path = CGM; + sourceTree = ""; + }; 43A943731B926B7B0051FA24 /* WatchApp */ = { isa = PBXGroup; children = ( @@ -842,6 +857,7 @@ 43F5C2E41B93C5D4003EB13D /* Managers */ = { isa = PBXGroup; children = ( + 439BED281E76091600B0AED5 /* CGM */, 439897361CD2F80600223065 /* AnalyticsManager.swift */, 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */, 43F4EF1C1BA2A57600526CE1 /* DiagnosticLogger.swift */, @@ -1238,13 +1254,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C1C73F081DE3D0260022FC89 /* InfoPlist.strings in Resources */, 43FCBBC21E51710B00343C1B /* LaunchScreen.storyboard in Resources */, 43776F991B8022E90074EA36 /* Assets.xcassets in Resources */, 434F54591D28805E002A9274 /* ButtonTableViewCell.xib in Resources */, - C1C73F021DE3D0250022FC89 /* Localizable.strings in Resources */, 43776F971B8022E90074EA36 /* Main.storyboard in Resources */, C9886AE51E5B2FAD00473BB8 /* gallery.ckcomplication in Resources */, + 4309786C1E73D2F500BEBC82 /* it.lproj in Resources */, 434F545B1D2880D4002A9274 /* AuthenticationTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1362,6 +1377,7 @@ 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, 437CCADA1D284ADF0075D2C3 /* AuthenticationTableViewCell.swift in Sources */, + 439BED2E1E760BC600B0AED5 /* EnliteCGMManager.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, 43F41C331D3A17AA00C11ED6 /* ChartAxisValueDoubleUnit.swift in Sources */, @@ -1393,10 +1409,12 @@ 43BFF0BC1E45C80600FF19A9 /* UIColor+Loop.swift in Sources */, 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */, 434FF1EE1CF27EEF000DB779 /* UITableViewCell.swift in Sources */, + 439BED2A1E76093C00B0AED5 /* CGMManager.swift in Sources */, C18C8C511D5A351900E043FB /* NightscoutDataManager.swift in Sources */, 438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */, 437CCADC1D284B830075D2C3 /* ButtonTableViewCell.swift in Sources */, 4315D2871CA5CC3B00589052 /* CarbEntryEditTableViewController.swift in Sources */, + 4309786E1E73DAD100BEBC82 /* CGM.swift in Sources */, 43F5173D1D713DB0000FA422 /* RadioSelectionTableViewController.swift in Sources */, 4331E0781C85302200FBE832 /* CGPoint.swift in Sources */, C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */, @@ -1424,6 +1442,7 @@ 540DED971E14C75F002B2491 /* EnliteSensorDisplayable.swift in Sources */, 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, 43E2D8C61D204678004DA55F /* KeychainManager.swift in Sources */, + 439BED2C1E760A7A00B0AED5 /* DexCGMManager.swift in Sources */, 433EA4C21D9F39C900CD78FB /* PumpIDTableViewController.swift in Sources */, 43BFF0B21E45C18400FF19A9 /* UIColor.swift in Sources */, 43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */, @@ -1484,7 +1503,6 @@ 43E2D8DB1D20C03B004DA55F /* NSTimeInterval.swift in Sources */, 43E2D8D41D20BF42004DA55F /* DoseMathTests.swift in Sources */, C11C87DE1E21EAAD00BB71D3 /* HKUnit.swift in Sources */, - C1C6591A1E1B1F430025CC58 /* (null) in Sources */, C17824A61E1AF91F00D9D25C /* BolusRecommendation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1609,22 +1627,6 @@ name = MainInterface.storyboard; sourceTree = ""; }; - C1C73F041DE3D0250022FC89 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - C1C73F031DE3D0250022FC89 /* it */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - C1C73F0A1DE3D0260022FC89 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - C1C73F091DE3D0260022FC89 /* it */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; C1C73F0F1DE3D0270022FC89 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index f2f0868f3c..25bd46728b 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -16,7 +16,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - private(set) lazy var dataManager = DeviceDataManager() + private(set) lazy var deviceManager = DeviceDataManager() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window?.tintColor = UIColor.tintColor @@ -27,7 +27,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { if let navVC = window?.rootViewController as? UINavigationController, let statusVC = navVC.viewControllers.first as? StatusTableViewController { - statusVC.dataManager = dataManager + statusVC.dataManager = deviceManager } return true @@ -49,8 +49,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - - dataManager.transmitter?.resumeScanning() } func applicationWillTerminate(_ application: UIApplication) { @@ -79,7 +77,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { { AnalyticsManager.sharedManager.didRetryBolus() - dataManager.enactBolus(units: units) { (error) in + deviceManager.enactBolus(units: units) { (error) in if error != nil { NotificationManager.sendBolusFailureNotificationForAmount(units, atStartDate: startDate) } diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index c1854b60f6..dc004524cf 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -15,19 +15,16 @@ extension UserDefaults { private enum Key: String { case BasalRateSchedule = "com.loudnate.Naterade.BasalRateSchedule" + case cgmSettings = "com.loopkit.Loop.cgmSettings" case CarbRatioSchedule = "com.loudnate.Naterade.CarbRatioSchedule" case ConnectedPeripheralIDs = "com.loudnate.Naterade.ConnectedPeripheralIDs" case DosingEnabled = "com.loudnate.Naterade.DosingEnabled" case InsulinActionDuration = "com.loudnate.Naterade.InsulinActionDuration" case InsulinSensitivitySchedule = "com.loudnate.Naterade.InsulinSensitivitySchedule" - case G4ReceiverEnabled = "com.loudnate.Loop.G4ReceiverEnabled" - case G5TransmitterEnabled = "com.loopkit.Loop.G5TransmitterEnabled" - case G5TransmitterID = "com.loudnate.Naterade.TransmitterID" case GlucoseTargetRangeSchedule = "com.loudnate.Naterade.GlucoseTargetRangeSchedule" case MaximumBasalRatePerHour = "com.loudnate.Naterade.MaximumBasalRatePerHour" case MaximumBolus = "com.loudnate.Naterade.MaximumBolus" case PreferredInsulinDataSource = "com.loudnate.Loop.PreferredInsulinDataSource" - case FetchEnliteDataEnabled = "com.loopkit.Loop.FetchEnliteDataEnabled" case PumpID = "com.loudnate.Naterade.PumpID" case PumpModelNumber = "com.loudnate.Naterade.PumpModelNumber" case PumpRegion = "com.loopkit.Loop.PumpRegion" @@ -63,6 +60,42 @@ extension UserDefaults { } } + var cgm: CGM? { + get { + if let rawValue = dictionary(forKey: Key.cgmSettings.rawValue) { + return CGM(rawValue: rawValue) + } else { + // Migrate the "version 0" case. Further format changes should be handled in the CGM initializer + defer { + removeObject(forKey: "com.loopkit.Loop.G5TransmitterEnabled") + removeObject(forKey: "com.loudnate.Loop.G4ReceiverEnabled") + removeObject(forKey: "com.loopkit.Loop.FetchEnliteDataEnabled") + removeObject(forKey: "com.loudnate.Naterade.TransmitterID") + } + + if bool(forKey: "com.loudnate.Loop.G4ReceiverEnabled") { + self.cgm = .g4 + return .g4 + } + + if bool(forKey: "com.loopkit.Loop.FetchEnliteDataEnabled") { + self.cgm = .enlite + return .enlite + } + + if let transmitterID = string(forKey: "com.loudnate.Naterade.TransmitterID"), transmitterID.characters.count == 6 { + self.cgm = .g5(transmitterID: transmitterID) + return .g5(transmitterID: transmitterID) + } + + return nil + } + } + set { + set(newValue?.rawValue, forKey: Key.cgmSettings.rawValue) + } + } + var connectedPeripheralIDs: [String] { get { return array(forKey: Key.ConnectedPeripheralIDs.rawValue) as? [String] ?? [] @@ -209,24 +242,6 @@ extension UserDefaults { } } - var receiverEnabled: Bool { - get { - return bool(forKey: Key.G4ReceiverEnabled.rawValue) - } - set { - set(newValue, forKey: Key.G4ReceiverEnabled.rawValue) - } - } - - var fetchEnliteDataEnabled: Bool { - get { - return bool(forKey: Key.FetchEnliteDataEnabled.rawValue) - } - set { - set(newValue, forKey: Key.FetchEnliteDataEnabled.rawValue) - } - } - var retrospectiveCorrectionEnabled: Bool { get { return bool(forKey: Key.RetrospectiveCorrectionEnabled.rawValue) @@ -236,31 +251,6 @@ extension UserDefaults { } } - var transmitterEnabled: Bool { - get { - if object(forKey: Key.G5TransmitterEnabled.rawValue) == nil { - // Old versions of Loop used the existence of transmitterID to indicate - // that the transmitter is enabled. Upgrade to the new format now. The - // transmitter is enabled if there's a 6 character transmitter ID - set(transmitterID?.characters.count == 6, forKey: Key.G5TransmitterEnabled.rawValue) - } - - return bool(forKey: Key.G5TransmitterEnabled.rawValue) - } - set { - set(newValue, forKey: Key.G5TransmitterEnabled.rawValue) - } - } - - var transmitterID: String? { - get { - return string(forKey: Key.G5TransmitterID.rawValue) - } - set { - set(newValue, forKey: Key.G5TransmitterID.rawValue) - } - } - var batteryChemistry: BatteryChemistryType? { get { return BatteryChemistryType(rawValue: integer(forKey: Key.BatteryChemistry.rawValue)) diff --git a/Loop/Extensions/NightscoutUploader.swift b/Loop/Extensions/NightscoutUploader.swift index 8d33aed343..ef4de90478 100644 --- a/Loop/Extensions/NightscoutUploader.swift +++ b/Loop/Extensions/NightscoutUploader.swift @@ -6,6 +6,9 @@ // import CarbKit +import CoreData +import InsulinKit +import MinimedKit import NightscoutUploadKit @@ -49,3 +52,30 @@ extension NightscoutUploader: CarbStoreSyncDelegate { } } } + + +extension NightscoutUploader { + func upload(_ events: [PersistedPumpEvent], from pumpModel: PumpModel, completion: @escaping (NightscoutUploadKit.Either<[NSManagedObjectID], Error>) -> Void) { + var objectIDs = [NSManagedObjectID]() + var timestampedPumpEvents = [TimestampedHistoryEvent]() + + for event in events { + objectIDs.append(event.objectID) + + if let raw = event.raw, raw.count > 0, let type = MinimedKit.PumpEventType(rawValue: raw[0])?.eventType, let pumpEvent = type.init(availableData: raw, pumpModel: pumpModel) { + timestampedPumpEvents.append(TimestampedHistoryEvent(pumpEvent: pumpEvent, date: event.date)) + } + } + + let nsEvents = NightscoutPumpEvents.translate(timestampedPumpEvents, eventSource: "loop://\(UIDevice.current.name)", includeCarbs: false) + + self.upload(nsEvents) { (result) in + switch result { + case .success( _): + completion(.success(objectIDs)) + case .failure(let error): + completion(.failure(error)) + } + } + } +} diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index c93592a7d7..5543d4568a 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -9,7 +9,6 @@ import Foundation import CarbKit import CoreData -import G4ShareSpy import GlucoseKit import HealthKit import InsulinKit @@ -18,11 +17,9 @@ import LoopUI import MinimedKit import NightscoutUploadKit import RileyLinkKit -import ShareClient -import xDripG5 -final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, TransmitterDelegate, ReceiverDelegate { +final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate { // MARK: - Utilities @@ -31,51 +28,11 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter /// Manages all the RileyLinks let rileyLinkManager: RileyLinkDeviceManager - /// Manages remote data + /// Manages authentication for remote services let remoteDataManager = RemoteDataManager() private var nightscoutDataManager: NightscoutDataManager! - // The Dexcom Share receiver object - private var receiver: Receiver? { - didSet { - receiver?.delegate = self - enableRileyLinkHeartbeatIfNeeded() - } - } - - var receiverEnabled: Bool { - get { - return receiver != nil - } - set { - receiver = newValue ? Receiver() : nil - UserDefaults.standard.receiverEnabled = newValue - } - } - - var transmitterEnabled: Bool { - get { - return UserDefaults.standard.transmitterEnabled - } - set { - return UserDefaults.standard.transmitterEnabled = newValue - } - } - - var fetchEnliteDataEnabled: Bool { - get { - return UserDefaults.standard.fetchEnliteDataEnabled - } - set { - UserDefaults.standard.fetchEnliteDataEnabled = newValue - } - } - - var sensorInfo: SensorDisplayable? { - return latestGlucoseG5 ?? latestGlucoseG4 ?? latestGlucoseFromShare ?? latestPumpStatusFromMySentry ?? latestEnliteData - } - var latestPumpStatus: RileyLinkKit.PumpStatus? // Returns a value in the range 0 - 1 @@ -141,13 +98,9 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - @objc private func receivedRileyLinkTimerTickNotification(_ note: Notification) { - backfillGlucoseFromShareIfNeeded() { - if UserDefaults.standard.fetchEnliteDataEnabled { - self.assertCurrentEnliteData() - } - - self.assertCurrentPumpData() + @objc private func receivedRileyLinkTimerTickNotification(_: Notification) { + cgmManager?.fetchNewDataIfNeeded(with: self) { (result) in + self.cgmManager(self.cgmManager!, didUpdateWith: result) } } @@ -171,20 +124,6 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - /// Controls the management of the RileyLink timer tick, which is a reliably-changing BLE - /// characteristic which can cause the app to wake. For most users, the G5 Transmitter and - /// G4 Receiver are reliable as hearbeats, but users who find their resources extremely constrained - /// due to greedy apps or older devices may choose to always enable the timer by always setting `true` - private func enableRileyLinkHeartbeatIfNeeded() { - if transmitter != nil { - rileyLinkManager.timerTickEnabled = false - } else if receiverEnabled { - rileyLinkManager.timerTickEnabled = false - } else { - rileyLinkManager.timerTickEnabled = true - } - } - // MARK: Pump data var latestPumpStatusFromMySentry: MySentryPumpStatusMessageBody? @@ -231,27 +170,35 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter // Trigger device status upload, even if something is wrong with pumpStatus nightscoutDataManager.uploadDeviceStatus(pumpStatus, rileylinkDevice: device) - backfillGlucoseFromShareIfNeeded() - - // Minimed sensor glucose switch status.glucose { case .active(glucose: let glucose): + // Enlite data is included if let date = glucoseDateComponents?.date { glucoseStore?.addGlucose( HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucose)), date: date, isDisplayOnly: false, device: nil - ) { (success, _, error) in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") - } - + ) { (success, _, _) in if success { NotificationCenter.default.post(name: .GlucoseUpdated, object: self) } } } + case .off: + // Enlite is disabled, so assert glucose from another source + cgmManager?.fetchNewDataIfNeeded(with: self) { (result) in + switch result { + case .newData(let values): + self.glucoseStore?.addGlucoseValues(values, device: self.cgmManager?.device) { (success, _, _) in + if success { + NotificationCenter.default.post(name: .GlucoseUpdated, object: self) + } + } + case .noData, .error: + break + } + } default: break } @@ -260,10 +207,9 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter remoteDataManager.nightscoutService.uploader?.uploadSGVFromMySentryPumpStatus(status, device: device.deviceURI) // Sentry packets are sent in groups of 3, 5s apart. Wait 11s before allowing the loop data to continue to avoid conflicting comms. - DispatchQueue.global(qos: DispatchQoS.QoSClass.utility).asyncAfter(deadline: DispatchTime.now() + Double(Int64(11 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { + DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(11)) { self.updateReservoirVolume(status.reservoirRemainingUnits, at: pumpDate, withTimeLeft: TimeInterval(minutes: Double(status.reservoirRemainingMinutes))) } - } /** @@ -313,7 +259,6 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - /** Polls the pump for new history events and stores them. @@ -325,8 +270,6 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter return } - print("Fetching history with RileyLink \"\(device.name)\"") - let startDate = doseStore.pumpEventQueryAfterDate device.ops?.getHistoryEvents(since: startDate) { (result) in @@ -392,7 +335,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter /** Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets. */ - private func assertCurrentPumpData() { + fileprivate func assertCurrentPumpData() { guard let device = rileyLinkManager.firstConnectedDevice, pumpDataIsStale() else { return } @@ -485,12 +428,9 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter - parameter device: The RileyLink device */ private func troubleshootPumpComms(using device: RileyLinkDevice) { - // How long we should wait before we re-tune the RileyLink let tuneTolerance = TimeInterval(minutes: 14) - print("auto-tune \(device.name)") - if device.lastTuned == nil || device.lastTuned!.timeIntervalSinceNow <= -tuneTolerance { device.tunePump { (result) in switch result { @@ -506,219 +446,33 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - // MARK: - Enlite - - fileprivate var latestEnliteData: EnliteSensorDisplayable? - - private func updateEnliteSensorStatus(_ events: [TimestampedGlucoseEvent]) { - let sensorEvents = events.filter({ $0.glucoseEvent is RelativeTimestampedGlucoseEvent }) - - if let latestSensorEvent = sensorEvents.last?.glucoseEvent as? RelativeTimestampedGlucoseEvent { - self.latestEnliteData = EnliteSensorDisplayable(latestSensorEvent) - } - } - - private func assertCurrentEnliteData() { - guard let device = rileyLinkManager.firstConnectedDevice, pumpDataIsStale() else { - return - } - - device.assertIdleListening() - - let fetchGlucoseSince = glucoseStore?.latestGlucose?.startDate.addingTimeInterval(TimeInterval(minutes: 1)) ?? Date(timeIntervalSinceNow: TimeInterval(hours: -24)) - - guard fetchGlucoseSince.timeIntervalSinceNow <= TimeInterval(minutes: -4.5) else { - return - } - - device.ops?.getGlucoseHistoryEvents(since: fetchGlucoseSince, completion: { (result) in - switch result { - case .success(let glucoseEvents): - - defer { - _ = self.remoteDataManager.nightscoutService.uploader?.processGlucoseEvents(glucoseEvents, source: device.deviceURI) - } - - self.updateEnliteSensorStatus(glucoseEvents) - - let glucoseValues = glucoseEvents - .filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > fetchGlucoseSince }) - .map({ (e:TimestampedGlucoseEvent) -> (quantity: HKQuantity, date: Date, isDisplayOnly: Bool) in - let glucoseEvent = e.glucoseEvent as! SensorValueGlucoseEvent - let quantity = HKQuantity(unit: HKUnit.milligramsPerDeciliterUnit(), doubleValue: Double(glucoseEvent.sgv)) - return (quantity: quantity, date: e.date, isDisplayOnly: false) - }) - - self.glucoseStore?.addGlucoseValues(glucoseValues, device: nil) { (success, _, error) in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") - } - - if success { - NotificationCenter.default.post(name: .GlucoseUpdated, object: self) - } - } - - case .failure(let error): - self.logger.addError(error, fromSource: "PumpOps") - } - }) - } - - // MARK: - G5 Transmitter - /// The G5 transmitter is a reliable heartbeat by which we can assert the loop state. - - // MARK: TransmitterDelegate - - func transmitter(_ transmitter: xDripG5.Transmitter, didError error: Error) { - logger.addMessage([ - "error": "\(error)", - "collectedAt": DateFormatter.ISO8601StrictDateFormatter().string(from: Date()) - ], toCollection: "g5" - ) - - assertCurrentPumpData() - } - - func transmitter(_ transmitter: xDripG5.Transmitter, didRead glucose: xDripG5.Glucose) { - assertCurrentPumpData() - - guard glucose != latestGlucoseG5 else { - return - } - - latestGlucoseG5 = glucose - - guard let glucoseStore = glucoseStore, let quantity = glucose.glucose else { - NotificationCenter.default.post(name: .GlucoseUpdated, object: self) - return - } - - let device = HKDevice(name: "xDripG5", manufacturer: "Dexcom", model: "G5 Mobile", hardwareVersion: nil, firmwareVersion: nil, softwareVersion: String(xDripG5VersionNumber), localIdentifier: nil, udiDeviceIdentifier: "00386270000002") + // MARK: - CGM - glucoseStore.addGlucose(quantity, date: glucose.readDate, isDisplayOnly: glucose.isDisplayOnly, device: device) { (success, _, error) -> Void in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") + var cgm: CGM? = UserDefaults.standard.cgm { + didSet { + if cgm != oldValue { + setupCGM() } - if success { - NotificationCenter.default.post(name: .GlucoseUpdated, object: self) - } + UserDefaults.standard.cgm = cgm } } - public func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) { - logger.addMessage([ - "unknownData": data.hexadecimalString, - "collectedAt": DateFormatter.ISO8601StrictDateFormatter().string(from: Date()) - ], toCollection: "g5" - ) - } - - // MARK: G5 data - - fileprivate var latestGlucoseG5: xDripG5.Glucose? - - fileprivate var latestGlucoseFromShare: ShareGlucose? - - /** - Attempts to backfill glucose data from the share servers if a G5 connection hasn't been established. - - - parameter completion: An optional closure called after the command is complete. - */ - private func backfillGlucoseFromShareIfNeeded(_ completion: (() -> Void)? = nil) { - // We should have no G4 Share or G5 data, and a configured ShareClient and GlucoseStore. - guard latestGlucoseG4 == nil && latestGlucoseG5 == nil, let shareClient = remoteDataManager.shareService.client, let glucoseStore = glucoseStore else { - completion?() - return - } - - // If our last glucose was less than 4.5 minutes ago, don't fetch. - if let latestGlucose = glucoseStore.latestGlucose, latestGlucose.startDate.timeIntervalSinceNow > -TimeInterval(minutes: 4.5) { - completion?() - return - } - - shareClient.fetchLast(6) { (error, glucose) in - guard let glucose = glucose else { - if let error = error { - self.logger.addError(error, fromSource: "ShareClient") - } - completion?() - return - } - - self.latestGlucoseFromShare = glucose.first - - // Ignore glucose values that are up to a minute newer than our previous value, to account for possible time shifting in Share data - let newGlucose = glucose.filterDateRange(glucoseStore.latestGlucose?.startDate.addingTimeInterval(TimeInterval(minutes: 1)), nil).map { - return (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: false) - } - - glucoseStore.addGlucoseValues(newGlucose, device: nil) { (success, _, error) -> Void in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") - } + private(set) var cgmManager: CGMManager? - if success { - NotificationCenter.default.post(name: .GlucoseUpdated, object: self) - } + private func setupCGM() { + cgmManager = cgm?.createManager() + cgmManager?.delegate = self - completion?() - } - } + /// Controls the management of the RileyLink timer tick, which is a reliably-changing BLE + /// characteristic which can cause the app to wake. For most users, the G5 Transmitter and + /// G4 Receiver are reliable as hearbeats, but users who find their resources extremely constrained + /// due to greedy apps or older devices may choose to always enable the timer by always setting `true` + rileyLinkManager.timerTickEnabled = !(cgmManager?.providesBLEHeartbeat == true) } - // MARK: - Share Receiver - - // MARK: ReceiverDelegate - - fileprivate var latestGlucoseG4: GlucoseG4? - - func receiver(_ receiver: Receiver, didReadGlucoseHistory glucoseHistory: [GlucoseG4]) { - assertCurrentPumpData() - - guard let latest = glucoseHistory.sorted(by: { $0.sequence < $1.sequence }).last, latest != latestGlucoseG4 else { - return - } - latestGlucoseG4 = latest - - guard let glucoseStore = glucoseStore else { - return - } - - // In the event that some of the glucose history was already backfilled from Share, don't overwrite it. - let includeAfter = glucoseStore.latestGlucose?.startDate.addingTimeInterval(TimeInterval(minutes: 1)) - - let validGlucose = glucoseHistory.flatMap({ - $0.isStateValid ? $0 : nil - }).filterDateRange(includeAfter, nil).map({ - (quantity: $0.quantity, date: $0.startDate, isDisplayOnly: $0.isDisplayOnly) - }) - - // "Dexcom G4 Platinum Transmitter (Retail) US" - see https://accessgudid.nlm.nih.gov/devices/search?query=dexcom+g4 - let device = HKDevice(name: "G4ShareSpy", manufacturer: "Dexcom", model: "G4 Share", hardwareVersion: nil, firmwareVersion: nil, softwareVersion: String(G4ShareSpyVersionNumber), localIdentifier: nil, udiDeviceIdentifier: "40386270000048") - - glucoseStore.addGlucoseValues(validGlucose, device: device) { (success, _, error) -> Void in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") - } - - if success { - NotificationCenter.default.post(name: .GlucoseUpdated, object: self) - } - } - } - - func receiver(_ receiver: Receiver, didError error: Error) { - logger.addMessage(["error": "\(error)", "collectedAt": DateFormatter.ISO8601StrictDateFormatter().string(from: Date())], toCollection: "g4") - - assertCurrentPumpData() - } - - func receiver(_ receiver: Receiver, didLogBluetoothEvent event: String) { - // Uncomment to debug communication - // logger.addMessage(["event": "\(event)", "collectedAt": NSDateFormatter.ISO8601StrictDateFormatter().stringFromDate(NSDate())], toCollection: "g4") + var sensorInfo: SensorDisplayable? { + return cgmManager?.sensorState ?? latestPumpStatusFromMySentry } // MARK: - Configuration @@ -820,39 +574,13 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - /// The Default battery chemistry is Alkaline + /// The pump battery chemistry, for voltage -> percentage calculation var batteryChemistry = UserDefaults.standard.batteryChemistry ?? .alkaline { didSet { UserDefaults.standard.batteryChemistry = batteryChemistry } } - // MARK: G5 Transmitter - - internal private(set) var transmitter: Transmitter? { - didSet { - transmitter?.delegate = self - enableRileyLinkHeartbeatIfNeeded() - } - } - - var transmitterID: String? { - get { - return transmitter?.ID - } - set { - guard transmitterID != newValue else { return } - - if let transmitterID = newValue, transmitterID.characters.count == 6 { - transmitter = Transmitter(ID: transmitterID, passiveModeEnabled: true) - } else { - transmitter = nil - } - - UserDefaults.standard.transmitterID = newValue - } - } - // MARK: Loop model inputs var basalRateSchedule: BasalRateSchedule? = UserDefaults.standard.basalRateSchedule { @@ -990,23 +718,10 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter return } - var objectIDs = [NSManagedObjectID]() - var timestampedPumpEvents = [TimestampedHistoryEvent]() - - for event in pumpEvents { - objectIDs.append(event.objectID) - - if let raw = event.raw, raw.count > 0, let type = MinimedKit.PumpEventType(rawValue: raw[0])?.eventType, let pumpEvent = type.init(availableData: raw, pumpModel: pumpModel) { - timestampedPumpEvents.append(TimestampedHistoryEvent(pumpEvent: pumpEvent, date: event.date)) - } - } - - let nsEvents = NightscoutPumpEvents.translate(timestampedPumpEvents, eventSource: "loop://\(UIDevice.current.name)", includeCarbs: false) - - uploader.upload(nsEvents) { (result) in + uploader.upload(pumpEvents, from: pumpModel) { (result) in switch result { - case .success( _): - completionHandler(objectIDs) + case .success(let objects): + completionHandler(objects) case .failure(let error): self.logger.addError(error, fromSource: "NightscoutUploadKit") completionHandler([]) @@ -1089,48 +804,53 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter carbStore?.syncDelegate = remoteDataManager.nightscoutService.uploader doseStore.delegate = self - if UserDefaults.standard.receiverEnabled { - receiver = Receiver() - receiver?.delegate = self - } - - if UserDefaults.standard.transmitterEnabled, - let transmitterID = UserDefaults.standard.transmitterID, - transmitterID.characters.count == 6 { - - transmitter = Transmitter(ID: transmitterID, passiveModeEnabled: true) - transmitter?.delegate = self - } - - enableRileyLinkHeartbeatIfNeeded() + setupCGM() } } extension DeviceDataManager: RemoteDataManagerDelegate { - func remoteDataManagerdidUpdateServices(_ dataManager: RemoteDataManager) { + func remoteDataManagerDidUpdateServices(_ dataManager: RemoteDataManager) { carbStore?.syncDelegate = dataManager.nightscoutService.uploader } } +extension DeviceDataManager: CGMManagerDelegate { + func cgmManager(_ manager: CGMManager, didUpdateWith result: CGMResult) { + switch result { + case .newData(let values): + glucoseStore?.addGlucoseValues(values, device: manager.device) { (success, _, _) in + if success { + NotificationCenter.default.post(name: .GlucoseUpdated, object: self) + } + + self.assertCurrentPumpData() + } + case .noData, .error: + self.assertCurrentPumpData() + } + } + + func startDateToFilterNewData(for manager: CGMManager) -> Date? { + return glucoseStore?.latestGlucose?.startDate + } +} + + extension DeviceDataManager: CustomDebugStringConvertible { var debugDescription: String { return [ "## DeviceDataManager", - "receiverEnabled: \(receiverEnabled)", + "cgm: \(cgm)", "latestPumpStatusFromMySentry: \(latestPumpStatusFromMySentry)", - "latestGlucoseG5: \(latestGlucoseG5)", - "latestGlucoseFromShare: \(latestGlucoseFromShare)", - "latestGlucoseG4: \(latestGlucoseG4)", "pumpState: \(String(reflecting: pumpState))", "preferredInsulinDataSource: \(preferredInsulinDataSource)", - "transmitterEnabled: \(transmitterEnabled)", - "transmitterID: \(transmitterID)", "glucoseTargetRangeSchedule: \(glucoseTargetRangeSchedule?.debugDescription ?? "")", "workoutModeEnabled: \(workoutModeEnabled)", "maximumBasalRatePerHour: \(maximumBasalRatePerHour)", "maximumBolus: \(maximumBolus)", + cgmManager != nil ? String(reflecting: cgmManager!) : "", String(reflecting: rileyLinkManager), String(reflecting: statusExtensionManager!), "", diff --git a/Loop/Managers/RemoteDataManager.swift b/Loop/Managers/RemoteDataManager.swift index 1f4939ab8f..d30c539656 100644 --- a/Loop/Managers/RemoteDataManager.swift +++ b/Loop/Managers/RemoteDataManager.swift @@ -19,7 +19,7 @@ final class RemoteDataManager { didSet { keychain.setNightscoutURL(nightscoutService.siteURL, secret: nightscoutService.APISecret) UIDevice.current.isBatteryMonitoringEnabled = true - delegate?.remoteDataManagerdidUpdateServices(self) + delegate?.remoteDataManagerDidUpdateServices(self) } } @@ -49,5 +49,5 @@ final class RemoteDataManager { protocol RemoteDataManagerDelegate: class { - func remoteDataManagerdidUpdateServices(_ dataManager: RemoteDataManager) + func remoteDataManagerDidUpdateServices(_ dataManager: RemoteDataManager) } diff --git a/Loop/Models/EnliteSensorDisplayable.swift b/Loop/Models/EnliteSensorDisplayable.swift index 37c50a5b91..aeb891616b 100644 --- a/Loop/Models/EnliteSensorDisplayable.swift +++ b/Loop/Models/EnliteSensorDisplayable.swift @@ -10,12 +10,29 @@ import Foundation import LoopUI import MinimedKit + struct EnliteSensorDisplayable: SensorDisplayable { public let isStateValid: Bool - public let trendType: LoopUI.GlucoseTrend? = nil - public let isLocal = true + public let trendType: LoopUI.GlucoseTrend? + public let isLocal: Bool + + public init?(_ event: RelativeTimestampedGlucoseEvent) { + isStateValid = event.isStateValid + trendType = event.trendType + isLocal = event.isLocal + } +} + +extension RelativeTimestampedGlucoseEvent { + var isStateValid: Bool { + return self is SensorValueGlucoseEvent + } + + var trendType: LoopUI.GlucoseTrend? { + return nil + } - public init?(_ e: RelativeTimestampedGlucoseEvent) { - isStateValid = e is SensorValueGlucoseEvent + var isLocal: Bool { + return true } } diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index e2a1ff0066..942c59fc4b 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -47,7 +47,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu dataManager.rileyLinkManager.deviceScanningEnabled = true - if dataManager.transmitterEnabled || dataManager.receiverEnabled, let glucoseStore = dataManager.glucoseStore, glucoseStore.authorizationRequired { + if case .some = dataManager.cgm, let glucoseStore = dataManager.glucoseStore, glucoseStore.authorizationRequired { glucoseStore.authorize({ (success, error) -> Void in // Do nothing for now }) @@ -97,10 +97,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } fileprivate enum CGMRow: Int, CaseCountable { - case fetchEnlite = 0 - case receiverEnabled - case transmitterEnabled - case transmitterID // optional, only displayed if transmitterEnabled + case enlite = 0 + case g4 + case g5 + case g5TransmitterID // only displayed if g5 switched on } fileprivate enum ConfigurationRow: Int, CaseCountable { @@ -144,9 +144,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case .pump: return PumpRow.count case .cgm: - if dataManager.transmitterEnabled { + switch dataManager.cgm { + case .g5?: return CGMRow.count - } else { + default: return CGMRow.count - 1 } case .configuration: @@ -167,10 +168,10 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case .dosing: let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell - switchCell.`switch`?.isOn = dataManager.loopManager.dosingEnabled + switchCell.switch?.isOn = dataManager.loopManager.dosingEnabled switchCell.titleLabel.text = NSLocalizedString("Closed Loop", comment: "The title text for the looping enabled switch cell") - switchCell.`switch`?.addTarget(self, action: #selector(dosingEnabledChanged(_:)), for: .valueChanged) + switchCell.switch?.addTarget(self, action: #selector(dosingEnabledChanged(_:)), for: .valueChanged) return switchCell case .preferredInsulinDataSource: @@ -200,53 +201,45 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } cell = configCell case .cgm: - if case .fetchEnlite = CGMRow(rawValue: indexPath.row)! { - let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell - - switchCell.`switch`?.isOn = dataManager.fetchEnliteDataEnabled - switchCell.titleLabel.text = NSLocalizedString("Fetch Enlite Data", comment: "The title text for the fetch enlite data enabled switch cell") - - switchCell.`switch`?.addTarget(self, action: #selector(fetchEnliteEnabledChanged(_:)), for: .valueChanged) - - return switchCell - } - - if case .receiverEnabled = CGMRow(rawValue: indexPath.row)! { - let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell - - switchCell.`switch`?.isOn = dataManager.receiverEnabled - switchCell.titleLabel.text = NSLocalizedString("G4 Share Receiver", comment: "The title text for the G4 Share Receiver enabled switch cell") + let row = CGMRow(rawValue: indexPath.row)! + switch row { + case .g5TransmitterID: + let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) - switchCell.`switch`?.addTarget(self, action: #selector(receiverEnabledChanged(_:)), for: .valueChanged) + configCell.textLabel?.text = NSLocalizedString("G5 Transmitter ID", comment: "The title text for the Dexcom G5 transmitter ID config value") - return switchCell - } + if case .g5(let transmitterID)? = dataManager.cgm { + configCell.detailTextLabel?.text = transmitterID ?? TapToSetString + } - if case .transmitterEnabled = CGMRow(rawValue: indexPath.row)! { + cell = configCell + default: let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell - switchCell.`switch`?.isOn = dataManager.transmitterEnabled - switchCell.titleLabel.text = NSLocalizedString("G5 Transmitter", comment: "The title text for the G5 Transmitter enabled switch cell") - - switchCell.`switch`?.addTarget(self, action: #selector(transmitterEnabledChanged(_:)), for: .valueChanged) + switch row { + case .enlite: + switchCell.switch?.isOn = dataManager.cgm == .enlite + switchCell.titleLabel.text = NSLocalizedString("Sof-Sensor / Enlite", comment: "The title text for the Medtronic sensor switch cell") + switchCell.switch?.addTarget(self, action: #selector(enliteChanged(_:)), for: .valueChanged) + case .g4: + switchCell.switch?.isOn = dataManager.cgm == .g4 + switchCell.titleLabel.text = NSLocalizedString("G4 Share Receiver", comment: "The title text for the G4 Share Receiver switch cell") + switchCell.switch?.addTarget(self, action: #selector(g4Changed(_:)), for: .valueChanged) + case .g5: + if case .g5? = dataManager.cgm { + switchCell.switch?.isOn = true + } else { + switchCell.switch?.isOn = false + } - return switchCell + switchCell.titleLabel.text = NSLocalizedString("G5 Transmitter", comment: "The title text for the G5 Transmitter switch cell") + switchCell.switch?.addTarget(self, action: #selector(g5Changed(_:)), for: .valueChanged) + case .g5TransmitterID: + assertionFailure() + } + cell = switchCell } - - let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) - switch CGMRow(rawValue: indexPath.row)! { - case .fetchEnlite: - break - case .transmitterEnabled: - break - case .transmitterID: - configCell.textLabel?.text = NSLocalizedString("G5 Transmitter ID", comment: "The title text for the Dexcom G5 transmitter ID config value") - configCell.detailTextLabel?.text = dataManager.transmitterID ?? TapToSetString - case .receiverEnabled: - break - } - cell = configCell case .configuration: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) @@ -428,28 +421,22 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu show(vc, sender: sender) } case .cgm: - let row = CGMRow(rawValue: indexPath.row)! - switch row { - case .fetchEnlite: - break - case .transmitterEnabled: - break - case .transmitterID: + switch CGMRow(rawValue: indexPath.row)! { + case .g5TransmitterID: let vc: TextFieldTableViewController + var value: String? - switch row { - case .transmitterID: - vc = .transmitterID(dataManager.transmitterID) - default: - fatalError() + if case .g5(let transmitterID)? = dataManager.cgm { + value = transmitterID } + vc = .transmitterID(value) vc.title = sender?.textLabel?.text vc.indexPath = indexPath vc.delegate = self show(vc, sender: indexPath) - case .receiverEnabled: + default: break } case .configuration: @@ -684,60 +671,76 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } } - @objc private func transmitterEnabledChanged(_ sender: UISwitch) { + // MARK: - CGM State + + // MARK: Model + + /// Temporarily caches the last transmitter ID so curious switch-flippers don't lose it! + private var g5TransmitterID: String? + + @objc private func g5Changed(_ sender: UISwitch) { + tableView.beginUpdates() if sender.isOn { - enableTransmitter() + setG4SwitchOff() + setEnliteSwitchOff() + dataManager.cgm = .g5(transmitterID: g5TransmitterID) + + tableView.insertRows(at: [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) } else { - disableTransmitter() + removeG5TransmitterIDRow() + dataManager.cgm = nil } + tableView.endUpdates() } - private func enableTransmitter() { - if dataManager.transmitterEnabled == false { - dataManager.transmitterEnabled = true - disableReceiver() - disableEnlite() - tableView.insertRows(at: [IndexPath(row: CGMRow.transmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) + @objc private func g4Changed(_ sender: UISwitch) { + tableView.beginUpdates() + if sender.isOn { + setG5SwitchOff() + setEnliteSwitchOff() + dataManager.cgm = .g4 + } else { + dataManager.cgm = nil } + tableView.endUpdates() } - private func disableTransmitter() { - if dataManager.transmitterEnabled { - dataManager.transmitterEnabled = false - let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.transmitterEnabled.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell - switchCell.`switch`?.setOn(false, animated: true) - tableView.deleteRows(at: [IndexPath(row: CGMRow.transmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) + @objc func enliteChanged(_ sender: UISwitch) { + tableView.beginUpdates() + if sender.isOn { + setG5SwitchOff() + setG4SwitchOff() + dataManager.cgm = .enlite + } else { + dataManager.cgm = nil } + tableView.endUpdates() } - @objc private func receiverEnabledChanged(_ sender: UISwitch) { - dataManager.receiverEnabled = sender.isOn + // MARK: Views - if sender.isOn { - disableTransmitter() - disableEnlite() + private func removeG5TransmitterIDRow() { + if case .g5(let transmitterID)? = dataManager.cgm { + g5TransmitterID = transmitterID + tableView.deleteRows(at: [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) } } - private func disableReceiver() { - dataManager.receiverEnabled = false - let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.receiverEnabled.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell - switchCell.`switch`?.setOn(false, animated: true) - } + private func setG5SwitchOff() { + let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.g5.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell + switchCell.switch?.setOn(false, animated: true) - func fetchEnliteEnabledChanged(_ sender: UISwitch) { - dataManager.fetchEnliteDataEnabled = sender.isOn + removeG5TransmitterIDRow() + } - if sender.isOn { - disableTransmitter() - disableReceiver() - } + private func setG4SwitchOff() { + let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.g4.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell + switchCell.switch?.setOn(false, animated: true) } - private func disableEnlite() { - dataManager.fetchEnliteDataEnabled = false - let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.fetchEnlite.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell - switchCell.`switch`?.setOn(false, animated: true) + private func setEnliteSwitchOff() { + let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.enlite.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell + switchCell.switch?.setOn(false, animated: true) } // MARK: - DailyValueScheduleTableViewControllerDelegate @@ -830,8 +833,14 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate { } case .cgm: switch CGMRow(rawValue: indexPath.row)! { - case .transmitterID: - dataManager.transmitterID = controller.value + case .g5TransmitterID: + var transmitterID = controller.value + + if transmitterID?.isEmpty ?? false { + transmitterID = nil + } + + dataManager.cgm = .g5(transmitterID: transmitterID) default: assertionFailure() } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index b38e2320cb..0b4c057622 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -827,7 +827,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize let glucoseTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openCGMApp(_:))) hudView.glucoseHUD.addGestureRecognizer(glucoseTapGestureRecognizer) - if cgmAppURL != nil { + if dataManager.cgm?.appURL != nil { hudView.glucoseHUD.accessibilityHint = NSLocalizedString("Launches CGM app", comment: "Glucose HUD accessibility hint") } @@ -840,16 +840,6 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } } - private var cgmAppURL: URL? { - if let url = URL(string: "dexcomcgm://"), UIApplication.shared.canOpenURL(url) { - return url - } else if let url = URL(string: "dexcomshare://"), UIApplication.shared.canOpenURL(url) { - return url - } else { - return nil - } - } - @objc private func showLastError(_: Any) { self.dataManager.loopManager.getLoopStatus { (_, _, _, _, _, _, _, error) -> Void in if let error = error { @@ -859,7 +849,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize } @objc private func openCGMApp(_: Any) { - if let url = cgmAppURL { + if let url = dataManager.cgm?.appURL, UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) } } diff --git a/Loop/Views/SwitchTableViewCell.swift b/Loop/Views/SwitchTableViewCell.swift index 0e39d762cc..bfe5b04297 100644 --- a/Loop/Views/SwitchTableViewCell.swift +++ b/Loop/Views/SwitchTableViewCell.swift @@ -27,7 +27,7 @@ final class SwitchTableViewCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() - `switch`?.removeTarget(nil, action: nil, for: .valueChanged) + self.switch?.removeTarget(nil, action: nil, for: .valueChanged) } }