generated from StanfordBDHG/SwiftPackageTemplate
-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move HealthMeasurements to SpeziDevices
- Loading branch information
Showing
30 changed files
with
963 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
Sources/SpeziDevices/Health/BloodPressureMeasurement+HKSample.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
import HealthKit | ||
import SpeziBluetoothServices | ||
|
||
|
||
extension BloodPressureMeasurement { | ||
public func bloodPressureSample(source device: HKDevice?) -> HKCorrelation? { | ||
guard systolicValue.isFinite, diastolicValue.isFinite else { | ||
return nil | ||
} | ||
let unit: HKUnit = unit.hkUnit | ||
|
||
let systolic = HKQuantity(unit: unit, doubleValue: systolicValue.double) | ||
let diastolic = HKQuantity(unit: unit, doubleValue: diastolicValue.double) | ||
|
||
let systolicType = HKQuantityType(.bloodPressureSystolic) | ||
let diastolicType = HKQuantityType(.bloodPressureDiastolic) | ||
let correlationType = HKCorrelationType(.bloodPressure) | ||
|
||
let date = timeStamp?.date ?? .now | ||
|
||
let systolicSample = HKQuantitySample(type: systolicType, quantity: systolic, start: date, end: date, device: device, metadata: nil) | ||
let diastolicSample = HKQuantitySample(type: diastolicType, quantity: diastolic, start: date, end: date, device: device, metadata: nil) | ||
|
||
|
||
let bloodPressure = HKCorrelation( | ||
type: correlationType, | ||
start: date, | ||
end: date, | ||
objects: [systolicSample, diastolicSample], | ||
device: device, | ||
metadata: nil | ||
) | ||
|
||
return bloodPressure | ||
} | ||
} | ||
|
||
|
||
extension BloodPressureMeasurement { | ||
public func heartRateSample(source device: HKDevice?) -> HKQuantitySample? { | ||
guard let pulseRate, pulseRate.isFinite else { | ||
return nil | ||
} | ||
|
||
// beats per minute | ||
let bpm: HKUnit = .count().unitDivided(by: .minute()) | ||
let pulseQuantityType = HKQuantityType(.heartRate) | ||
|
||
let pulse = HKQuantity(unit: bpm, doubleValue: pulseRate.double) | ||
let date = timeStamp?.date ?? .now | ||
|
||
return HKQuantitySample( | ||
type: pulseQuantityType, | ||
quantity: pulse, | ||
start: date, | ||
end: date, | ||
device: device, | ||
metadata: nil | ||
) | ||
} | ||
} | ||
|
||
|
||
#if DEBUG || TEST | ||
extension HKCorrelation { | ||
@_spi(TestingSupport) | ||
public static var mockBloodPressureSample: HKCorrelation { | ||
Check warning on line 76 in Sources/SpeziDevices/Health/BloodPressureMeasurement+HKSample.swift GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane
|
||
let measurement = BloodPressureMeasurement(systolic: 117, diastolic: 76, meanArterialPressure: 67, unit: .mmHg, pulseRate: 68) | ||
guard let sample = measurement.bloodPressureSample(source: nil) else { | ||
preconditionFailure("Mock sample was unexpectedly invalid!") | ||
} | ||
return sample | ||
} | ||
} | ||
|
||
extension HKQuantitySample { | ||
@_spi(TestingSupport) | ||
public static var mockHeartRateSample: HKQuantitySample { | ||
Check warning on line 87 in Sources/SpeziDevices/Health/BloodPressureMeasurement+HKSample.swift GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane
|
||
let measurement = BloodPressureMeasurement(systolic: 117, diastolic: 76, meanArterialPressure: 67, unit: .mmHg, pulseRate: 68) | ||
guard let sample = measurement.heartRateSample(source: nil) else { | ||
preconditionFailure("Mock sample was unexpectedly invalid!") | ||
} | ||
return sample | ||
} | ||
} | ||
#endif |
25 changes: 25 additions & 0 deletions
25
Sources/SpeziDevices/Health/BloodPressureMeasurement.Unit+HKUnit.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
|
||
import HealthKit | ||
import SpeziBluetoothServices | ||
|
||
|
||
extension BloodPressureMeasurement.Unit { | ||
/// The unit represented as a `HKUnit`. | ||
public var hkUnit: HKUnit { | ||
switch self { | ||
case .mmHg: | ||
return .millimeterOfMercury() | ||
case .kPa: | ||
return .pascalUnit(with: .kilo) | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
Sources/SpeziDevices/Health/WeightMeasurement+HKSample.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
import HealthKit | ||
import SpeziBluetoothServices | ||
|
||
|
||
extension WeightMeasurement { | ||
public func quantitySample(source device: HKDevice?, resolution: WeightScaleFeature.WeightResolution?) -> HKQuantitySample { | ||
Check warning on line 15 in Sources/SpeziDevices/Health/WeightMeasurement+HKSample.swift GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane
|
||
let quantityType = HKQuantityType(.bodyMass) | ||
|
||
let value = weight(of: resolution ?? .unspecified) | ||
|
||
let quantity = HKQuantity(unit: unit.massUnit, doubleValue: value) | ||
let date = timeStamp?.date ?? .now | ||
|
||
return HKQuantitySample( | ||
type: quantityType, | ||
quantity: quantity, | ||
start: date, | ||
end: date, | ||
device: device, | ||
metadata: nil | ||
) | ||
} | ||
} | ||
|
||
#if DEBUG || TEST | ||
extension HKQuantitySample { | ||
@_spi(TestingSupport) | ||
public static var mockWeighSample: HKQuantitySample { | ||
let measurement = WeightMeasurement(weight: 8400, unit: .si) | ||
|
||
return measurement.quantitySample(source: nil, resolution: .resolution5g) | ||
} | ||
} | ||
#endif |
35 changes: 35 additions & 0 deletions
35
Sources/SpeziDevices/Health/WeightMeasurement.Unit+HKUnit.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
|
||
import HealthKit | ||
import SpeziBluetoothServices | ||
|
||
|
||
extension WeightMeasurement.Unit { | ||
/// The mass unit represented as a `HKUnit`. | ||
public var massUnit: HKUnit { | ||
switch self { | ||
case .si: | ||
return .gramUnit(with: .kilo) | ||
case .imperial: | ||
return .pound() | ||
} | ||
} | ||
|
||
|
||
/// The length unit represented as a `HKUnit`. | ||
public var lengthUnit: HKUnit { | ||
switch self { | ||
case .si: | ||
return .meter() | ||
case .imperial: | ||
return .inch() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// | ||
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import SpeziBluetoothServices | ||
|
||
|
||
public enum HealthMeasurement { | ||
case weight(WeightMeasurement, WeightScaleFeature) | ||
case bloodPressure(BloodPressureMeasurement, BloodPressureFeature) | ||
} |
121 changes: 121 additions & 0 deletions
121
Sources/SpeziDevices/Measurements/HealthMeasurements.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// | ||
// This source file is part of the ENGAGE-HF project based on the Stanford Spezi Template Application project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import Foundation | ||
import OSLog | ||
import Spezi | ||
import SpeziBluetooth | ||
#if DEBUG | ||
@_spi(TestingSupport) | ||
#endif | ||
import SpeziDevices | ||
|
||
|
||
/// Manage and process incoming health measurements. | ||
@Observable | ||
public class HealthMeasurements: Module, EnvironmentAccessible, DefaultInitializable { | ||
private let logger = Logger(subsystem: "ENGAGEHF", category: "HealthMeasurements") | ||
|
||
var newMeasurement: ProcessedHealthMeasurement? | ||
|
||
@StandardActor @ObservationIgnored private var standard: any HealthMeasurementsConstraint | ||
@Dependency @ObservationIgnored private var bluetooth: Bluetooth? | ||
|
||
required public init() {} | ||
|
||
// TODO: rename! | ||
public func handleNewMeasurement<Device: HealthDevice>(_ measurement: HealthMeasurement, from device: Device) { | ||
let hkDevice = device.hkDevice | ||
|
||
switch measurement { | ||
case let .weight(measurement, feature): | ||
let sample = measurement.quantitySample(source: hkDevice, resolution: feature.weightResolution) | ||
logger.debug("Measurement loaded: \(measurement.weight)") | ||
|
||
newMeasurement = .weight(sample) | ||
case let .bloodPressure(measurement, _): | ||
let bloodPressureSample = measurement.bloodPressureSample(source: hkDevice) | ||
let heartRateSample = measurement.heartRateSample(source: hkDevice) | ||
|
||
guard let bloodPressureSample else { | ||
logger.debug("Discarding invalid blood pressure measurement ...") | ||
return | ||
} | ||
|
||
logger.debug("Measurement loaded: \(String(describing: measurement))") | ||
|
||
newMeasurement = .bloodPressure(bloodPressureSample, heartRate: heartRateSample) | ||
} | ||
} | ||
|
||
// TODO: docs everywhere! | ||
|
||
public func saveMeasurement() async throws { // TODO: rename? | ||
if ProcessInfo.processInfo.isPreviewSimulator { | ||
try await Task.sleep(for: .seconds(5)) | ||
return | ||
} | ||
|
||
guard let measurement = self.newMeasurement else { | ||
logger.error("Attempting to save a nil measurement.") | ||
return | ||
} | ||
|
||
logger.info("Saving the following measurement: \(String(describing: measurement))") | ||
do { | ||
switch measurement { | ||
case let .weight(sample): | ||
try await standard.addMeasurement(sample: sample) | ||
case let .bloodPressure(bloodPressureSample, heartRateSample): | ||
try await standard.addMeasurement(sample: bloodPressureSample) | ||
if let heartRateSample { | ||
try await standard.addMeasurement(sample: heartRateSample) | ||
} | ||
} | ||
} catch { | ||
logger.error("Failed to save measurement samples: \(error)") | ||
throw error | ||
} | ||
|
||
logger.info("Save successful!") | ||
newMeasurement = nil | ||
} | ||
} | ||
|
||
|
||
#if DEBUG || TEST | ||
extension HealthMeasurements { | ||
/// Call in preview simulator wrappers. | ||
/// | ||
/// Loads a mock measurement to display in preview. | ||
@_spi(TestingSupport) | ||
public func loadMockWeightMeasurement() { | ||
let device = MockDevice.createMockDevice() | ||
|
||
guard let measurement = device.weightScale.weightMeasurement else { | ||
preconditionFailure("Mock Weight Measurement was never injected!") | ||
} | ||
|
||
handleNewMeasurement(.weight(measurement, device.weightScale.features ?? []), from: device) | ||
} | ||
|
||
/// Call in preview simulator wrappers. | ||
/// | ||
/// Loads a mock measurement to display in preview. | ||
@_spi(TestingSupport) | ||
public func loadMockBloodPressureMeasurement() { | ||
let device = MockDevice.createMockDevice() | ||
|
||
guard let measurement = device.bloodPressure.bloodPressureMeasurement else { | ||
preconditionFailure("Mock Blood Pressure Measurement was never injected!") | ||
} | ||
|
||
handleNewMeasurement(.bloodPressure(measurement, device.bloodPressure.features ?? []), from: device) | ||
} | ||
} | ||
#endif |
15 changes: 15 additions & 0 deletions
15
Sources/SpeziDevices/Measurements/HealthMeasurementsConstraint.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// | ||
// This source file is part of the Stanford Spezi open-source project | ||
// | ||
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) | ||
// | ||
// SPDX-License-Identifier: MIT | ||
// | ||
|
||
import HealthKit | ||
import Spezi | ||
|
||
|
||
public protocol HealthMeasurementsConstraint: Standard { | ||
func addMeasurement(sample: HKSample) async throws | ||
} |
Oops, something went wrong.