Skip to content

Commit

Permalink
Improve ECG Uploading & Re-Upload Incomplete ECGs
Browse files Browse the repository at this point in the history
  • Loading branch information
PSchmiedmayer committed Aug 26, 2024
1 parent 1fffc5b commit 7fd9ea5
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 39 deletions.
18 changes: 6 additions & 12 deletions PAWS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
2F65B44E2A3B8B0600A36932 /* NotificationPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */; };
2FA0BFED2ACC977500E0EF83 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */; };
2FA25D492B8402A70016E4C7 /* HKElectrocardiogram+SupplementaryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA25D482B8402A70016E4C7 /* HKElectrocardiogram+SupplementaryData.swift */; };
2FA7EC1C2C7C1CFB00D674AA /* FHIRTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7EC1B2C7C1CFB00D674AA /* FHIRTypes.swift */; };
2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099AE2A875DF100B20952 /* FirebaseAuth */; };
2FB099B12A875DF100B20952 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B02A875DF100B20952 /* FirebaseFirestore */; };
2FB099B32A875DF100B20952 /* FirebaseFirestoreSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB099B22A875DF100B20952 /* FirebaseFirestoreSwift */; };
Expand Down Expand Up @@ -110,6 +111,7 @@
2F65B44D2A3B8B0600A36932 /* NotificationPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissions.swift; sourceTree = "<group>"; };
2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
2FA25D482B8402A70016E4C7 /* HKElectrocardiogram+SupplementaryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKElectrocardiogram+SupplementaryData.swift"; sourceTree = "<group>"; };
2FA7EC1B2C7C1CFB00D674AA /* FHIRTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRTypes.swift; sourceTree = "<group>"; };
2FAEC07F297F583900C11C42 /* PAWS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PAWS.entitlements; sourceTree = "<group>"; };
2FBF63322C2CF491002F4AC1 /* Firestore+User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Firestore+User.swift"; sourceTree = "<group>"; };
2FC94CD4298B0A1D009C8209 /* PAWS.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PAWS.xctestplan; sourceTree = "<group>"; };
Expand Down Expand Up @@ -296,6 +298,7 @@
2FE5DC4429EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift */,
B2433E1C2BCF60C800D7C798 /* Contact+PersonNameComponents.swift */,
2FBF63322C2CF491002F4AC1 /* Firestore+User.swift */,
2FA7EC1B2C7C1CFB00D674AA /* FHIRTypes.swift */,
);
path = Helper;
sourceTree = "<group>";
Expand Down Expand Up @@ -419,7 +422,6 @@
);
dependencies = (
2FDF443B2C65DB4A0075AFC1 /* PBXTargetDependency */,
2FDF443D2C65DB4A0075AFC1 /* PBXTargetDependency */,
);
name = PAWS;
packageProductDependencies = (
Expand Down Expand Up @@ -623,6 +625,7 @@
B2F7F1E22BA549A900BE93BE /* InvitationCodeError.swift in Sources */,
2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */,
2F65B44E2A3B8B0600A36932 /* NotificationPermissions.swift in Sources */,
2FA7EC1C2C7C1CFB00D674AA /* FHIRTypes.swift in Sources */,
5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */,
2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */,
2FA25D492B8402A70016E4C7 /* HKElectrocardiogram+SupplementaryData.swift in Sources */,
Expand Down Expand Up @@ -655,10 +658,6 @@
isa = PBXTargetDependency;
productRef = 2FDF443A2C65DB4A0075AFC1 /* SwiftPackageListJSONPlugin */;
};
2FDF443D2C65DB4A0075AFC1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 2FDF443C2C65DB4A0075AFC1 /* SwiftLintPlugin */;
};
653A255F28338800005D4D48 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 653A254C283387FE005D4D48 /* PAWS */;
Expand Down Expand Up @@ -1023,8 +1022,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/StanfordBDHG/HealthKitOnFHIR.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.2.7;
branch = feature/ecgPrecision;
kind = branch;
};
};
2FCC1DCE2B6A2CE000C686BE /* XCRemoteSwiftPackageReference "SwiftLint" */ = {
Expand Down Expand Up @@ -1165,11 +1164,6 @@
package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */;
productName = "plugin:SwiftPackageListJSONPlugin";
};
2FDF443C2C65DB4A0075AFC1 /* SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 2FCC1DCE2B6A2CE000C686BE /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin";
};
2FE5DC6329EDD883004B9AB4 /* SpeziAccount */ = {
isa = XCSwiftPackageProductDependency;
package = 2FE5DC6229EDD883004B9AB4 /* XCRemoteSwiftPackageReference "SpeziAccount" */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git",
"state" : {
"revision" : "b0cfe35a2263a517b22196b559d2dd1d1e2afcd9",
"version" : "0.2.9"
"branch" : "feature/ecgPrecision",
"revision" : "4465c3b01d6fa8dda2fb2bf9e5254b804bfb29ba"
}
},
{
Expand Down
52 changes: 38 additions & 14 deletions PAWS/ECGRecordings/ECGModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
import FirebaseAuth
import FirebaseFirestore
import HealthKit
import HealthKitOnFHIR
import OSLog
import enum ModelsR4.ResourceProxy
import Spezi
import SpeziFirebaseConfiguration
import SpeziHealthKit
import SpeziLocalStorage
import UserNotifications


@globalActor fileprivate actor ECGModuleActor: GlobalActor {

Check failure on line 21 in PAWS/ECGRecordings/ECGModule.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Private over Fileprivate Violation: Prefer `private` over `fileprivate` declarations (private_over_fileprivate)
static let shared = ECGModuleActor()
}

@Observable
class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
@ObservationIgnored @Dependency private var firebaseConfiguration: ConfigureFirebaseApp
@ObservationIgnored @Dependency private var healthKit: HealthKit
@ObservationIgnored @Dependency(ConfigureFirebaseApp.self) private var firebaseConfiguration
@ObservationIgnored @Dependency(HealthKit.self) private var healthKit

private(set) var electrocardiograms: [HKElectrocardiogram] = []
private var authStateDidChangeListenerHandle: AuthStateDidChangeListenerHandle?
Expand All @@ -48,13 +52,31 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
func isUploaded(_ electrocardiogram: HKElectrocardiogram, reuploadIfNeeded: Bool = false) async throws -> Bool {
let documentReference = try await Firestore.firestore().healthKitCollectionReference.document(electrocardiogram.uuid.uuidString)
let snapshot = try await documentReference.getDocument()

guard !snapshot.exists else {

/// This function is intended to re-upload ECGs that have not been completely uploaded. Could be removed in the future.
func voltageComplete(_ electrocardiogramObservation: FHIRObservation) -> Bool {
guard let ecgCode = HKElectrocardiogramMapping.default.voltageMeasurements.codings.first else {
return false
}

let voltageMeasurementsComponentsCount = electrocardiogramObservation.component?.count(where: { component in
return component.code.coding?.contains(where: { coding in

Check failure on line 63 in PAWS/ECGRecordings/ECGModule.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Implicit Return Violation: Prefer implicit returns in closures, functions and getters (implicit_return)
coding.code?.value?.string == ecgCode.code && coding.system?.value?.url == ecgCode.system
}) ?? false
})

return (voltageMeasurementsComponentsCount ?? 0) >= 3
}

if snapshot.exists,
let electrocardiogramObservation = try? snapshot.data(as: FHIRObservation.self),
voltageComplete(electrocardiogramObservation) {
return true
}

if reuploadIfNeeded {
try await documentReference.setData(from: try electrocardiogram.resource)
await upload(electrocardiogram: electrocardiogram)
logger.log("Uploaded Missing ECG: \(electrocardiogram.id)")
return true
}

Expand Down Expand Up @@ -83,7 +105,7 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
let samples = try await queryDescriptor.result(for: healthStore)

self.electrocardiograms = samples
try await self.uploadUnuploadedECGs()
await self.uploadUnuploadedECGs()
}


Expand Down Expand Up @@ -132,13 +154,15 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
}
}

@ECGModuleActor
func remove(sample id: HKSample.ID) async throws {
electrocardiograms.removeAll(where: { $0.uuid == id })
try await Firestore.firestore().healthKitCollectionReference.document(id.uuidString).delete()
}


// MARK: - Private Helper Functions
@ECGModuleActor
private func insert(electrocardiogram: HKElectrocardiogram) {
electrocardiograms.removeAll(where: { $0.uuid == electrocardiogram.id })
electrocardiograms.append(electrocardiogram)
Expand Down Expand Up @@ -181,9 +205,9 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {


private func upload(sample: HKSample, force: Bool = false) async throws {
let resource: ResourceProxy
let resource: FHIRResourceProxy
if let electrocardiogram = sample as? HKElectrocardiogram {
self.insert(electrocardiogram: electrocardiogram)
await self.insert(electrocardiogram: electrocardiogram)

guard try await !self.isUploaded(electrocardiogram) || force else {
return
Expand All @@ -192,7 +216,7 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
async let symptoms = try electrocardiogram.symptoms(from: healthStore)
async let voltageMeasurements = try electrocardiogram.voltageMeasurements(from: healthStore)

resource = ResourceProxy(
resource = FHIRResourceProxy(
with: try await electrocardiogram.observation(
symptoms: symptoms,
voltageMeasurements: voltageMeasurements
Expand All @@ -205,14 +229,14 @@ class ECGModule: Module, DefaultInitializable, EnvironmentAccessible {
try await Firestore.firestore().healthKitCollectionReference.document(sample.id.uuidString).setData(from: resource)
}

private func uploadUnuploadedECGs() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
for ecg in electrocardiograms where try await !isUploaded(ecg) {
private func uploadUnuploadedECGs() async {
await withTaskGroup(of: Void.self) { group in
for ecg in electrocardiograms {
group.addTask { [weak self] in
do {
try await self?.upload(sample: ecg)
} catch {
self?.logger.log("Could not access HealthKit sample: \(error)")
self?.logger.log("Could not upload ECG: \(error)")
await self?.addECGMessage(for: ecg, error: error)
}
}
Expand Down
20 changes: 10 additions & 10 deletions PAWS/ECGRecordings/ECGRecording.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ struct ECGRecording: View {
}
.padding()
}
.task {
guard let symptoms = try? await electrocardiogram.symptoms(from: HKHealthStore()) else {
return
}

self.symptoms = symptoms

if !FeatureFlags.disableFirebase {
self.isUploaded = (try? await ecgModule.isUploaded(electrocardiogram, reuploadIfNeeded: true)) ?? false
.task {
guard let symptoms = try? await electrocardiogram.symptoms(from: HKHealthStore()) else {
return
}

self.symptoms = symptoms

if !FeatureFlags.disableFirebase {
self.isUploaded = (try? await ecgModule.isUploaded(electrocardiogram, reuploadIfNeeded: true)) ?? false
}
}
}
}
}
13 changes: 13 additions & 0 deletions PAWS/Helper/FHIRTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// This source file is part of the PAWS application based on the Stanford Spezi Template Application project
//
// SPDX-FileCopyrightText: 2023 Stanford University
//
// SPDX-License-Identifier: MIT
//

import ModelsR4


typealias FHIRObservation = ModelsR4.Observation
typealias FHIRResourceProxy = ModelsR4.ResourceProxy
1 change: 0 additions & 1 deletion PAWS/PAWSStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import FirebaseAuth
import FirebaseFirestore
import FirebaseStorage
import HealthKitOnFHIR
import enum ModelsR4.ResourceProxy
import OSLog
import PDFKit
import Spezi
Expand Down

0 comments on commit 7fd9ea5

Please sign in to comment.