From a84c3c6f5d7d1278aacf60aeddf0c72991ca789c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 21 Sep 2023 23:26:20 -0500 Subject: [PATCH] Adding CGM Event Store --- Loop/Managers/DeviceDataManager.swift | 54 ++++--- Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/RemoteDataServicesManager.swift | 152 ++++++------------ Loop/Managers/SettingsManager.swift | 2 +- Scripts/capture-build-details.sh | 3 +- 5 files changed, 88 insertions(+), 125 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2e8d157531..95ef432094 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -157,6 +157,8 @@ final class DeviceDataManager { let glucoseStore: GlucoseStore + let cgmEventStore: CgmEventStore + private let cacheStore: PersistenceController let dosingDecisionStore: DosingDecisionStore @@ -337,11 +339,13 @@ final class DeviceDataManager { cgmStalenessMonitor = CGMStalenessMonitor() cgmStalenessMonitor.delegate = glucoseStore + + cgmEventStore = CgmEventStore(cacheStore: cacheStore, cacheLength: localCacheDuration) + + dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - self.dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - - self.cgmHasValidSensorSession = false - self.pumpIsAllowingAutomation = true + cgmHasValidSensorSession = false + pumpIsAllowingAutomation = true self.automaticDosingStatus = automaticDosingStatus // HealthStorePreferredGlucoseUnitDidChange will be notified once the user completes the health access form. Set to .milligramsPerDeciliter until then @@ -405,6 +409,7 @@ final class DeviceDataManager { doseStore: doseStore, dosingDecisionStore: dosingDecisionStore, glucoseStore: glucoseStore, + cgmEventStore: cgmEventStore, settingsStore: settingsManager.settingsStore, overrideHistory: overrideHistory, insulinDeliveryStore: doseStore.insulinDeliveryStore @@ -435,6 +440,7 @@ final class DeviceDataManager { doseStore.delegate = self dosingDecisionStore.delegate = self glucoseStore.delegate = self + cgmEventStore.delegate = self doseStore.insulinDeliveryStore.delegate = self remoteDataServicesManager.delegate = self @@ -934,6 +940,16 @@ extension DeviceDataManager: CGMManagerDelegate { } } + func cgmManager(_ manager: LoopKit.CGMManager, hasNew events: [PersistedCgmEvent]) { + Task { + do { + try await cgmEventStore.add(events: events) + } catch { + self.log.error("Error storing cgm events: %{public}@", error.localizedDescription) + } + } + } + func startDateToFilterNewData(for manager: CGMManager) -> Date? { dispatchPrecondition(condition: .onQueue(queue)) return glucoseStore.latestGlucose?.startDate @@ -1191,60 +1207,56 @@ extension DeviceDataManager: PumpManagerOnboardingDelegate { // MARK: - AlertStoreDelegate extension DeviceDataManager: AlertStoreDelegate { - func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - remoteDataServicesManager.alertStoreHasUpdatedAlertData(alertStore) + remoteDataServicesManager.triggerUpload(for: .alert) } - } // MARK: - CarbStoreDelegate extension DeviceDataManager: CarbStoreDelegate { - func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - remoteDataServicesManager.carbStoreHasUpdatedCarbData(carbStore) + remoteDataServicesManager.triggerUpload(for: .carb) } func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {} - } // MARK: - DoseStoreDelegate extension DeviceDataManager: DoseStoreDelegate { - func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - remoteDataServicesManager.doseStoreHasUpdatedPumpEventData(doseStore) + remoteDataServicesManager.triggerUpload(for: .pumpEvent) } - } // MARK: - DosingDecisionStoreDelegate extension DeviceDataManager: DosingDecisionStoreDelegate { - func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - remoteDataServicesManager.dosingDecisionStoreHasUpdatedDosingDecisionData(dosingDecisionStore) + remoteDataServicesManager.triggerUpload(for: .dosingDecision) } - } // MARK: - GlucoseStoreDelegate extension DeviceDataManager: GlucoseStoreDelegate { - func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - remoteDataServicesManager.glucoseStoreHasUpdatedGlucoseData(glucoseStore) + remoteDataServicesManager.triggerUpload(for: .glucose) } - } // MARK: - InsulinDeliveryStoreDelegate extension DeviceDataManager: InsulinDeliveryStoreDelegate { - func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - remoteDataServicesManager.insulinDeliveryStoreHasUpdatedDoseData(insulinDeliveryStore) + remoteDataServicesManager.triggerUpload(for: .dose) } +} +// MARK: - CgmEventStoreDelegate +extension DeviceDataManager: CgmEventStoreDelegate { + func cgmEventStoreHasUpdatedData(_ cgmEventStore: LoopKit.CgmEventStore) { + remoteDataServicesManager.triggerUpload(for: .cgmEvent) + } } + // MARK: - TestingPumpManager extension DeviceDataManager { func deleteTestingPumpData(completion: ((Error?) -> Void)? = nil) { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 9edf481ba2..95a7ed6d2d 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -603,7 +603,7 @@ extension LoopAppManager: TemporaryScheduleOverrideHistoryDelegate { func temporaryScheduleOverrideHistoryDidUpdate(_ history: TemporaryScheduleOverrideHistory) { UserDefaults.appGroup?.overrideHistory = history - deviceDataManager.remoteDataServicesManager.temporaryScheduleOverrideHistoryDidUpdate() + deviceDataManager.remoteDataServicesManager.triggerUpload(for: .overrides) } } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 14a3416900..3b86cf7f1e 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -10,13 +10,14 @@ import os.log import Foundation import LoopKit -enum RemoteDataType: String { +enum RemoteDataType: String, CaseIterable { case alert = "Alert" case carb = "Carb" case dose = "Dose" case dosingDecision = "DosingDecision" case glucose = "Glucose" case pumpEvent = "PumpEvent" + case cgmEvent = "CgmEvent" case settings = "Settings" case overrides = "Overrides" @@ -129,6 +130,8 @@ final class RemoteDataServicesManager { private let glucoseStore: GlucoseStore + private let cgmEventStore: CgmEventStore + private let insulinDeliveryStore: InsulinDeliveryStore private let settingsStore: SettingsStore @@ -141,6 +144,7 @@ final class RemoteDataServicesManager { doseStore: DoseStore, dosingDecisionStore: DosingDecisionStore, glucoseStore: GlucoseStore, + cgmEventStore: CgmEventStore, settingsStore: SettingsStore, overrideHistory: TemporaryScheduleOverrideHistory, insulinDeliveryStore: InsulinDeliveryStore @@ -150,6 +154,7 @@ final class RemoteDataServicesManager { self.doseStore = doseStore self.dosingDecisionStore = dosingDecisionStore self.glucoseStore = glucoseStore + self.cgmEventStore = cgmEventStore self.insulinDeliveryStore = insulinDeliveryStore self.settingsStore = settingsStore self.overrideHistory = overrideHistory @@ -167,13 +172,11 @@ final class RemoteDataServicesManager { } private func clearQueryAnchors(for remoteDataService: RemoteDataService) { - clearAlertQueryAnchor(for: remoteDataService) - clearCarbQueryAnchor(for: remoteDataService) - clearDoseQueryAnchor(for: remoteDataService) - clearDosingDecisionQueryAnchor(for: remoteDataService) - clearGlucoseQueryAnchor(for: remoteDataService) - clearPumpEventQueryAnchor(for: remoteDataService) - clearSettingsQueryAnchor(for: remoteDataService) + for remoteDataType in RemoteDataType.allCases { + dispatchQueue(for: remoteDataService, withRemoteDataType: remoteDataType).async { + UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: remoteDataType) + } + } } func triggerUpload(for triggeringType: RemoteDataType) { @@ -195,6 +198,8 @@ final class RemoteDataServicesManager { remoteDataServices.forEach { self.uploadGlucoseData(to: $0) } case .pumpEvent: remoteDataServices.forEach { self.uploadPumpEventData(to: $0) } + case .cgmEvent: + remoteDataServices.forEach { self.uploadCgmEventData(to: $0) } case .settings: remoteDataServices.forEach { self.uploadSettingsData(to: $0) } case .overrides: @@ -220,11 +225,6 @@ final class RemoteDataServicesManager { } extension RemoteDataServicesManager { - - public func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - triggerUpload(for: .alert) - } - private func uploadAlertData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -257,21 +257,9 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } - - private func clearAlertQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .alert).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .alert) - } - } - } extension RemoteDataServicesManager { - - public func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - triggerUpload(for: .carb) - } - private func uploadCarbData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -311,21 +299,9 @@ extension RemoteDataServicesManager { } } } - - private func clearCarbQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .carb).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .carb) - } - } - } extension RemoteDataServicesManager { - - public func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - triggerUpload(for: .dose) - } - private func uploadDoseData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -365,21 +341,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dose) - } - } - } extension RemoteDataServicesManager { - - public func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - triggerUpload(for: .dosingDecision) - } - private func uploadDosingDecisionData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -419,21 +383,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDosingDecisionQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dosingDecision).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dosingDecision) - } - } - } extension RemoteDataServicesManager { - - public func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - triggerUpload(for: .glucose) - } - private func uploadGlucoseData(to remoteDataService: RemoteDataService) { if delegate?.shouldSyncToRemoteService == false { @@ -478,21 +430,9 @@ extension RemoteDataServicesManager { } } } - - private func clearGlucoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .glucose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose) - } - } - } extension RemoteDataServicesManager { - - public func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - triggerUpload(for: .pumpEvent) - } - private func uploadPumpEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -532,21 +472,9 @@ extension RemoteDataServicesManager { } } } - - private func clearPumpEventQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent) - } - } - } extension RemoteDataServicesManager { - - public func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - triggerUpload(for: .settings) - } - private func uploadSettingsData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -586,21 +514,9 @@ extension RemoteDataServicesManager { } } } - - private func clearSettingsQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .settings) - } - } - } extension RemoteDataServicesManager { - - public func temporaryScheduleOverrideHistoryDidUpdate() { - triggerUpload(for: .overrides) - } - private func uploadTemporaryOverrideData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -629,10 +545,46 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } +} - private func clearTemporaryOverrideQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .overrides) +extension RemoteDataServicesManager { + private func uploadCgmEventData(to remoteDataService: RemoteDataService) { + uploadGroup.enter() + + let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + + dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { + let semaphore = DispatchSemaphore(value: 0) + let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent) ?? CgmEventStore.QueryAnchor() + var continueUpload = false + + self.cgmEventStore.executeCgmEventQuery(fromQueryAnchor: previousQueryAnchor) { result in + switch result { + case .failure(let error): + self.log.error("Error querying pump event data: %{public}@", String(describing: error)) + semaphore.signal() + case .success(let queryAnchor, let data): + remoteDataService.uploadCgmEventData(data) { result in + switch result { + case .failure(let error): + self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) + self.uploadFailed(key) + case .success: + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + self.uploadSucceeded(key) + } + semaphore.signal() + } + } + } + + semaphore.wait() + self.uploadGroup.leave() + + if continueUpload { + self.uploadPumpEventData(to: remoteDataService) + } } } } diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index f7421b97f7..e3fdb60bf7 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -211,7 +211,7 @@ class SettingsManager { // MARK: - SettingsStoreDelegate extension SettingsManager: SettingsStoreDelegate { func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - remoteDataServicesManager?.settingsStoreHasUpdatedSettingsData(settingsStore) + remoteDataServicesManager?.triggerUpload(for: .settings) } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..6122592374 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -10,10 +10,9 @@ SCRIPT_DIRECTORY="$(dirname "${0}")" error() { echo "ERROR: ${*}" >&2 - echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path] [-i|--info-plist-path info-plist-path]" >&2 + echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path]" >&2 echo "Parameters:" >&2 echo " -p|--provisioning-profile-path path to the .mobileprovision provisioning profile file to check for expiration; optional, defaults to \${HOME}/Library/MobileDevice/Provisioning Profiles/\${EXPANDED_PROVISIONING_PROFILE}.mobileprovision" >&2 - echo " -i|--info-plist-path path to the Info.plist file to modify; optional, defaults to \${BUILT_PRODUCTS_DIR}/\${INFOPLIST_PATH}" >&2 exit 1 }