Skip to content

Commit

Permalink
Fix Unknown Fields (#78)
Browse files Browse the repository at this point in the history
# Fix Unknown Fields

## ♻️ Current situation & Problem
- The viewer crashes if there is no heart rate or other information
available for an ECG.


## ⚙️ Release Notes 
- Fix Unknown Fields


### Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordBDHG/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordBDHG/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
PSchmiedmayer authored Aug 26, 2024
1 parent 96136ad commit 56c6521
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 57 deletions.
26 changes: 15 additions & 11 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 @@ -418,8 +421,8 @@
buildRules = (
);
dependencies = (
2F15E22E2C7C3F6A006584DD /* PBXTargetDependency */,
2FDF443B2C65DB4A0075AFC1 /* PBXTargetDependency */,
2FDF443D2C65DB4A0075AFC1 /* PBXTargetDependency */,
);
name = PAWS;
packageProductDependencies = (
Expand Down Expand Up @@ -623,6 +626,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 @@ -651,13 +655,13 @@
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
2FDF443B2C65DB4A0075AFC1 /* PBXTargetDependency */ = {
2F15E22E2C7C3F6A006584DD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 2FDF443A2C65DB4A0075AFC1 /* SwiftPackageListJSONPlugin */;
productRef = 2F15E22D2C7C3F6A006584DD /* SwiftLintPlugin */;
};
2FDF443D2C65DB4A0075AFC1 /* PBXTargetDependency */ = {
2FDF443B2C65DB4A0075AFC1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 2FDF443C2C65DB4A0075AFC1 /* SwiftLintPlugin */;
productRef = 2FDF443A2C65DB4A0075AFC1 /* SwiftPackageListJSONPlugin */;
};
653A255F28338800005D4D48 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
Expand Down Expand Up @@ -1024,7 +1028,7 @@
repositoryURL = "https://github.com/StanfordBDHG/HealthKitOnFHIR.git";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 0.2.7;
minimumVersion = 0.2.10;
};
};
2FCC1DCE2B6A2CE000C686BE /* XCRemoteSwiftPackageReference "SwiftLint" */ = {
Expand Down Expand Up @@ -1126,6 +1130,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
2F15E22D2C7C3F6A006584DD /* SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 2FCC1DCE2B6A2CE000C686BE /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin";
};
2F3D4ABB2A4E7C290068FB2F /* SpeziScheduler */ = {
isa = XCSwiftPackageProductDependency;
package = 2F3D4ABA2A4E7C290068FB2F /* XCRemoteSwiftPackageReference "SpeziScheduler" */;
Expand Down Expand Up @@ -1165,11 +1174,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"
"revision" : "c24e316311ff9813cb1fe32cd8820bcca6e5e7f2",
"version" : "0.2.10"
}
},
{
Expand Down
53 changes: 39 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 private actor ECGModuleActor: GlobalActor {
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,32 @@ 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
}

// Unfortunately we have to support compiler with explicity type annotations on slower machines & the CI.
let voltageMeasurementsComponentsCount = electrocardiogramObservation.component?.filter { component in
component.code.coding?.contains(where: { coding in
coding.code?.value?.string == ecgCode.code && coding.system?.value?.url == ecgCode.system
}) ?? false
}.count ?? 0

return voltageMeasurementsComponentsCount >= 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 +106,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 +155,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 +206,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 +217,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 +230,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
}
}
}
}
}
15 changes: 15 additions & 0 deletions PAWS/Helper/FHIRTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// 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
typealias FHIRObservationComponent = ModelsR4.ObservationComponent
typealias FHIRCoding = ModelsR4.Coding
7 changes: 3 additions & 4 deletions 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 All @@ -24,9 +23,9 @@ import SwiftUI


actor PAWSStandard: Standard, EnvironmentAccessible, HealthKitConstraint, OnboardingConstraint, AccountNotifyConstraint, AccountStorageConstraint {
@Dependency private var firebaseConfiguration: ConfigureFirebaseApp
@Dependency private var accountStorage: FirestoreAccountStorage?
@Dependency private var ecgStorage: ECGModule
@Dependency(ConfigureFirebaseApp.self) private var firebaseConfiguration
@Dependency(FirestoreAccountStorage.self) private var accountStorage: FirestoreAccountStorage?
@Dependency(ECGModule.self) private var ecgStorage

private let logger = Logger(subsystem: "PAWS", category: "Standard")

Expand Down
2 changes: 1 addition & 1 deletion PAWS/Study Information/EnrollmentGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import SwiftUI

@Observable
class EnrollmentGroup: Module, EnvironmentAccessible {
@ObservationIgnored @Dependency private var configureFirebaseApp: ConfigureFirebaseApp
@ObservationIgnored @Dependency(ConfigureFirebaseApp.self) private var configureFirebaseApp
private var dateOfBirth: Date?
private var authStateDidChangeListenerHandle: AuthStateDidChangeListenerHandle?
private var snapshotListener: ListenerRegistration?
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ Copy the `ECGExporter` and `ECGReviewer` notebooks in Colab Enterprise, uncommen
## Continous Integration Setup

The project supports different GitHub environments (`development`, `staging`, and `production`).
- The Firebase proejct ID needs to be saved as a GitHub variable with the name `FIREBASE_PROJECT_ID` for the different deployment environments.
- The Firebase project ID needs to be saved as a GitHub variable with the name `FIREBASE_PROJECT_ID` for the different deployment environments.
- The service account key needs to be added to the GitHub secrets as `GOOGLE_APPLICATION_CREDENTIALS_BASE64` in a base64 encoding to enable the beta deployment.
- To report code coverage, a CodeCov token should be added as a `CODECOV_TOKEN` environment secret.
- The Firebase Google plist needs to be stored as a base64 encoded secret named `GOOGLE_SERVICE_INFO_PLIST_BASE64`.
Expand Down
41 changes: 30 additions & 11 deletions ecg_data_manager/modules/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,21 @@ def plot_single_ecg(self, row): # pylint: disable=too-many-locals
ax=axs[i],
)

user_id = row[ColumnNames.USER_ID.value]
heart_rate = int(row[ColumnNames.HEART_RATE.value])
ecg_interpretation = row[
ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value
]
user_id = (
row[ColumnNames.USER_ID.value]
if row[ColumnNames.USER_ID.value] is not None
else "Unknown"
)
heart_rate = (
int(row[ColumnNames.HEART_RATE.value])
if row[ColumnNames.HEART_RATE.value] is not None
else "Unknown"
)
ecg_interpretation = (
row[ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value]
if row[ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value] is not None
else "Unknown"
)

group_class = row[AGE_GROUP_STRING]
user_id_html = widgets.HTML(
Expand Down Expand Up @@ -596,7 +606,6 @@ def save_diagnosis( # pylint: disable=too-many-locals, too-many-arguments
"saved successfully.✓</span>"
)
display(data_saved_html)

else:
print(
"ECG has already been reviewed. No further review is required."
Expand Down Expand Up @@ -929,11 +938,21 @@ def plot_single_ecg(self, row): # pylint: disable=too-many-locals
ax=axs[i],
)

user_id = row[ColumnNames.USER_ID.value]
heart_rate = int(row[ColumnNames.HEART_RATE.value])
ecg_interpretation = row[
ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value
]
user_id = (
row[ColumnNames.USER_ID.value]
if row[ColumnNames.USER_ID.value] is not None
else "Unknown"
)
heart_rate = (
int(row[ColumnNames.HEART_RATE.value])
if row[ColumnNames.HEART_RATE.value] is not None
else "Unknown"
)
ecg_interpretation = (
row[ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value]
if row[ColumnNames.APPLE_ELECTROCARDIOGRAM_CLASSIFICATION.value] is not None
else "Unknown"
)

group_class = row[AGE_GROUP_STRING]
user_id_html = widgets.HTML(
Expand Down
6 changes: 3 additions & 3 deletions firebase/firebase-export-metadata.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"version": "13.15.0",
"version": "13.15.4",
"firestore": {
"version": "1.19.7",
"path": "firestore_export",
"metadata_file": "firestore_export/firestore_export.overall_export_metadata"
},
"auth": {
"version": "13.15.0",
"version": "13.15.4",
"path": "auth_export"
},
"storage": {
"version": "13.15.0",
"version": "13.15.4",
"path": "storage_export"
}
}
Binary file not shown.

0 comments on commit 56c6521

Please sign in to comment.