From ecb3539645bf82d6a1c8c241b23c0bfe99dae61a Mon Sep 17 00:00:00 2001 From: Paul Plant Date: Tue, 12 Mar 2024 14:54:59 +0000 Subject: [PATCH] initial heartbeat UI implementation for data source --- .../xcshareddata/swiftpm/Package.resolved | 3 ++- ...excomG7HeartbeatBluetoothTransmitter.swift | 2 ++ .../Libre3HeartbeatBluetoothTransmitter.swift | 5 +++++ ...OmniPodHeartbeatBluetoothTransmitter.swift | 4 ++++ xdrip/Extensions/UserDefaults.swift | 17 ++++++++++++++++ xdrip/Managers/Alerts/AlertManager.swift | 4 ++-- .../FollowerBackgroundKeepAliveType.swift | 20 +++++++++++++++++++ .../LibreLinkUpFollowManager.swift | 7 ++++--- .../NightScout/NightScoutFollowManager.swift | 4 ++-- xdrip/Texts/TextsSettingsView.swift | 8 ++++++++ .../RootViewController.swift | 16 ++++++++++++++- ...tingsViewDataSourceSettingsViewModel.swift | 2 ++ 12 files changed, 83 insertions(+), 9 deletions(-) diff --git a/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved b/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved index b1785113..c71e659b 100644 --- a/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/xdrip.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "20a19467224593c1dcb0789843c73f4580a23d8227b26f5a2c54d6c62c75927e", "pins" : [ { "identity" : "actionclosurable", @@ -37,5 +38,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift index 24bf710b..48b6408e 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/DexcomG7HeartbeatBluetoothTransmitter.swift @@ -74,6 +74,8 @@ class DexcomG7HeartbeatBluetoothTransmitter: BluetoothTransmitter { self.bluetoothTransmitterDelegate?.heartBeat() lastHeartBeatTimeStamp = Date() + + UserDefaults.standard.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift index 753e117d..174073db 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/Libre3HeartbeatBluetoothTransmitter.swift @@ -67,6 +67,8 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { lastHeartBeatTimeStamp = Date() + UserDefaults.standard.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp + } } @@ -85,6 +87,9 @@ class Libre3HeartBeatBluetoothTransmitter: BluetoothTransmitter { lastHeartBeatTimeStamp = Date() + UserDefaults.standard.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp + + print("heatbeat timestamp: \(lastHeartBeatTimeStamp)") } } diff --git a/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift b/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift index 6b79b41d..a445c2c6 100644 --- a/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift +++ b/xdrip/BluetoothTransmitter/HeartBeat/OmniPodHeartbeatBluetoothTransmitter.swift @@ -68,6 +68,8 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { lastHeartBeatTimeStamp = Date() + UserDefaults.standard.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp + } } @@ -85,6 +87,8 @@ class OmniPodHeartBeatTransmitter: BluetoothTransmitter { lastHeartBeatTimeStamp = Date() + UserDefaults.standard.lastHeartBeatTimeStamp = lastHeartBeatTimeStamp + } } diff --git a/xdrip/Extensions/UserDefaults.swift b/xdrip/Extensions/UserDefaults.swift index ab5268c4..1cd20dbb 100644 --- a/xdrip/Extensions/UserDefaults.swift +++ b/xdrip/Extensions/UserDefaults.swift @@ -394,6 +394,10 @@ extension UserDefaults { /// - stored as data as read from transmitter case librePatchInfo = "librePatchInfo" + // heartbeat + /// the last heartbeat connection timestamp + case lastHeartBeatTimeStamp = "lastHeartBeatTimeStamp" + } @@ -2191,6 +2195,19 @@ extension UserDefaults { } } + + // MARK: - Heartbeat + + /// timestamp of last successful connection to follower service + @objc dynamic var lastHeartBeatTimeStamp: Date? { + get { + return object(forKey: Key.lastHeartBeatTimeStamp.rawValue) as? Date + } + set { + set(newValue, forKey: Key.lastHeartBeatTimeStamp.rawValue) + } + } + } diff --git a/xdrip/Managers/Alerts/AlertManager.swift b/xdrip/Managers/Alerts/AlertManager.swift index c9d6bede..b7a764be 100644 --- a/xdrip/Managers/Alerts/AlertManager.swift +++ b/xdrip/Managers/Alerts/AlertManager.swift @@ -719,8 +719,8 @@ public class AlertManager:NSObject { return true } else { - if !UserDefaults.standard.isMaster && UserDefaults.standard.followerBackgroundKeepAliveType == .disabled { - trace("in checkAlert, there's no need to raise alert %{public}@ because we're in follower mode and keep-alive is disabled", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) + if !UserDefaults.standard.isMaster && !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { + trace("in checkAlert, there's no need to raise alert %{public}@ because we're in follower mode and keep-alive is: %[public]@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), UserDefaults.standard.followerBackgroundKeepAliveType.description) } else { trace("in checkAlert, there's no need to raise alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging()) } diff --git a/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift b/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift index e769f4d0..89513bad 100644 --- a/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift +++ b/xdrip/Managers/Followers/FollowerBackgroundKeepAliveType.swift @@ -20,6 +20,7 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { case disabled = 0 case normal = 1 case aggressive = 2 + case heartbeat = 3 var description: String { switch self { @@ -29,6 +30,8 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { return Texts_SettingsView.followerKeepAliveTypeNormal case .aggressive: return Texts_SettingsView.followerKeepAliveTypeAggressive + case .heartbeat: + return Texts_SettingsView.followerKeepAliveTypeHeartbeat } } @@ -40,6 +43,19 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { return 1 case .aggressive: return 2 + case .heartbeat: + return 3 + } + } + + // return true if in follower mode and if the keep-alive type should provoke a background keep-alive action + // basically if not .disabled and if not .heartbeat + var shouldKeepAlive: Bool { + switch self { + case .disabled, .heartbeat: + return false + default: + return true } } @@ -52,6 +68,8 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { return UIImage(systemName: "n.circle") ?? UIImage() case .aggressive: return UIImage(systemName: "a.circle") ?? UIImage() + case .heartbeat: + return UIImage(systemName: "heart.circle") ?? UIImage() } } @@ -64,6 +82,8 @@ public enum FollowerBackgroundKeepAliveType: Int, CaseIterable { return Image(systemName: "n.circle") case .aggressive: return Image(systemName: "a.circle") + case .heartbeat: + return Image(systemName: "heart.circle") } } diff --git a/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift b/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift index 490888fd..6542713d 100644 --- a/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift +++ b/xdrip/Managers/LibreLinkUp/LibreLinkUpFollowManager.swift @@ -758,9 +758,9 @@ class LibreLinkUpFollowManager: NSObject { private func enableSuspensionPrevention() { // if keep-alive is disabled or if using a heartbeat, then just return and do nothing - if UserDefaults.standard.followerBackgroundKeepAliveType == .disabled || UserDefaults.standard.followerBackgroundKeepAliveType == { + if !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { - print("not enabling suspension prevention as keep-alive is disabled") + print("not enabling suspension prevention as keep-alive type is: \(UserDefaults.standard.followerBackgroundKeepAliveType.description)") return @@ -803,7 +803,8 @@ class LibreLinkUpFollowManager: NSObject { if !UserDefaults.standard.isMaster && UserDefaults.standard.followerDataSourceType == .libreLinkUp && UserDefaults.standard.libreLinkUpEmail != nil && UserDefaults.standard.libreLinkUpPassword != nil { // this will enable the suspension prevention sound playing if background keep-alive is enabled - if UserDefaults.standard.followerBackgroundKeepAliveType != .disabled { + // (i.e. not disabled and not using a heartbeat) + if UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { enableSuspensionPrevention() } else { disableSuspensionPrevention() diff --git a/xdrip/Managers/NightScout/NightScoutFollowManager.swift b/xdrip/Managers/NightScout/NightScoutFollowManager.swift index b0cebd71..d8a1f9bb 100644 --- a/xdrip/Managers/NightScout/NightScoutFollowManager.swift +++ b/xdrip/Managers/NightScout/NightScoutFollowManager.swift @@ -365,8 +365,8 @@ class NightScoutFollowManager: NSObject { /// launches timer that will regular play sound - this will be played only when app goes to background and only if the user wants to keep the app alive private func enableSuspensionPrevention() { - // if keep-alive is disabled, then just return and do nothing - if UserDefaults.standard.followerBackgroundKeepAliveType == .disabled { + // if keep-alive is not needed, then just return and do nothing + if !UserDefaults.standard.followerBackgroundKeepAliveType.shouldKeepAlive { print("not enabling suspension prevention as keep-alive is disabled") diff --git a/xdrip/Texts/TextsSettingsView.swift b/xdrip/Texts/TextsSettingsView.swift index af89f8cf..f0270b1f 100644 --- a/xdrip/Texts/TextsSettingsView.swift +++ b/xdrip/Texts/TextsSettingsView.swift @@ -128,6 +128,10 @@ class Texts_SettingsView { return NSLocalizedString("settingsviews_followerKeepAliveTypeAggressive", tableName: filename, bundle: Bundle.main, value: "Aggressive", comment: "data source settings, keep-alive mode is set to aggressive") }() + static let followerKeepAliveTypeHeartbeat: String = { + return NSLocalizedString("settingsviews_followerKeepAliveTypeHeartbeat", tableName: filename, bundle: Bundle.main, value: "Heartbeat ♥", comment: "data source settings, keep-alive mode is set to use an external heartbeat") + }() + static let followerKeepAliveTypeDisabledMessage: String = { return NSLocalizedString("settingsviews_followerKeepAliveTypeDisabledMessage", tableName: filename, bundle: Bundle.main, value: "Background keep-alive is disabled.\n\nWhen the app is not on screen, no alarms, app badges, notifications or BG updates will take place.\n\nThe app will remain sleeping until you open it again.\n\nThis mode has very little impact on the battery of your device.", comment: "data source settings, keep-alive mode is set to disabled") }() @@ -140,6 +144,10 @@ class Texts_SettingsView { return NSLocalizedString("settingsviews_followerKeepAliveTypeAggressiveMessage", tableName: filename, bundle: Bundle.main, value: "Background keep-alive is set to aggressive.\n\nWhen the app is not on screen, we will aggressively attempt to keep it running for you in the background so that BG updates are received and alarms can be triggered.\n\nThis mode has a very noticeable impact on the battery of your device and should only be used if absolutely necessary.", comment: "data source settings, keep-alive mode is set to aggressive") }() + static let followerKeepAliveTypeHeartbeatMessage: String = { + return NSLocalizedString("settingsviews_followerKeepAliveTypeHeartbeatMessage", tableName: filename, bundle: Bundle.main, value: "Background keep-alive is set to use an external heartbeat. ❤️\n\nWhen the app is not on screen, the external heartbeat will wake it up in the background so that BG updates are received and alarms can be triggered.\n\nMake sure you add a heartbeat device in the Bluetooth screen.\n\nThis mode has very little impact on the battery of your device but will only work if a valid heartbeat is running.", comment: "data source settings, keep-alive mode is set to use an external heartbeat") + }() + static let followerPatientName: String = { return NSLocalizedString("settingsviews_followerPatientName", tableName: filename, bundle: Bundle.main, value: "Patient Name", comment: "data source settings, the name of the person we are following") }() diff --git a/xdrip/View Controllers/Root View Controller/RootViewController.swift b/xdrip/View Controllers/Root View Controller/RootViewController.swift index 47c2d324..2efa88ed 100644 --- a/xdrip/View Controllers/Root View Controller/RootViewController.swift +++ b/xdrip/View Controllers/Root View Controller/RootViewController.swift @@ -919,6 +919,9 @@ final class RootViewController: UIViewController, ObservableObject { // add observer for showAppleWatchDebug, to reload the watch app view if the value changes UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.showAppleWatchDebug.rawValue, options: .new, context: nil) + + // add observer for the last heartbeat timestamp in order to update the UI + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.lastHeartBeatTimeStamp.rawValue, options: .new, context: nil) // setup delegate for UNUserNotificationCenter UNUserNotificationCenter.current().delegate = self @@ -1692,8 +1695,10 @@ final class RootViewController: UIViewController, ObservableObject { } case UserDefaults.Key.showAppleWatchDebug: - watchManager?.updateWatchApp() + + case UserDefaults.Key.lastHeartBeatTimeStamp: + updateDataSourceInfo(animate: false) default: break @@ -3237,6 +3242,15 @@ final class RootViewController: UIViewController, ObservableObject { // let's go through the specific cases for follower modes if !isMaster { + // first set the heartbeat icon color depending on when the last heartbeat was received + if UserDefaults.standard.followerBackgroundKeepAliveType == .heartbeat { + if let lastHeartBeatTimeStamp = UserDefaults.standard.lastHeartBeatTimeStamp, lastHeartBeatTimeStamp < Date().addingTimeInterval(-30) { + dataSourceKeepAliveImageOutlet.tintColor = .systemRed + } else { + dataSourceKeepAliveImageOutlet.tintColor = .systemGreen + } + } + dataSourceLabelOutlet.text = UserDefaults.standard.followerDataSourceType.fullDescription switch UserDefaults.standard.followerDataSourceType { diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDataSourceSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDataSourceSettingsViewModel.swift index 13cb16da..dac8f150 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDataSourceSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewDataSourceSettingsViewModel.swift @@ -249,6 +249,8 @@ class SettingsViewDataSourceSettingsViewModel: NSObject, SettingsViewModelProtoc message += Texts_SettingsView.followerKeepAliveTypeNormalMessage case .aggressive: message += Texts_SettingsView.followerKeepAliveTypeAggressiveMessage + case .heartbeat: + message += Texts_SettingsView.followerKeepAliveTypeHeartbeatMessage } self.callMessageHandlerInMainThread(title: Texts_SettingsView.labelfollowerKeepAliveType, message: message)