Skip to content

Commit

Permalink
Move HealthMeasurements to SpeziDevices
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Jun 22, 2024
1 parent 6ca3147 commit 4ac6757
Show file tree
Hide file tree
Showing 30 changed files with 963 additions and 117 deletions.
6 changes: 1 addition & 5 deletions Sources/SpeziDevices/DeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@ import SpeziBluetooth
import SpeziBluetoothServices
import SwiftUI

// TODO: Start SpeziDevices generalization
// TODO: Finish SpeziBluetooth refactoring and cleanup "persistent devices"

Check warning on line 15 in Sources/SpeziDevices/DeviceManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (Finish SpeziBluetooth refactor...) (todo)
// TODO: dark mode device images

Check warning on line 16 in Sources/SpeziDevices/DeviceManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved (dark mode device images) (todo)
// TODO: ask for more Omron infos? secret sauce?

// TODO: move deviceManager to SpeziBluetooth (and measurement manager?)

@Observable
public final class DeviceManager: Module, EnvironmentAccessible, DefaultInitializable {
public final class DeviceManager: Module, EnvironmentAccessible, DefaultInitializable { // TODO: "PairedDevices" rename?

Check warning on line 19 in Sources/SpeziDevices/DeviceManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved ("PairedDevices" rename?) (todo)
/// Determines if the device discovery sheet should be presented.
@MainActor public var presentingDevicePairing = false // TODO: "should" naming

Check warning on line 21 in Sources/SpeziDevices/DeviceManager.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Todo Violation: TODOs should be resolved ("should" naming) (todo)
@MainActor public private(set) var discoveredDevices: OrderedDictionary<UUID, any PairableDevice> = [:]
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziDevices/Devices/GenericDevice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SpeziBluetoothServices
/// A generic Bluetooth device.
///
/// A generic Bluetooth device that provides access to basic device information.
public protocol GenericDevice: BluetoothDevice, GenericBluetoothPeripheral {
public protocol GenericDevice: BluetoothDevice, GenericBluetoothPeripheral, Identifiable {
/// The device identifier.
///
/// Use the [`DeviceState`](https://swiftpackageindex.com/stanfordspezi/spezibluetooth/documentation/spezibluetooth/devicestate) property wrapper to
Expand Down
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

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)
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

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Attributes Violation: Attributes should be on their own lines in functions and types, but on the same line as variables and imports (attributes)
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
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 Sources/SpeziDevices/Health/WeightMeasurement+HKSample.swift
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

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package tvOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)

Check warning on line 15 in Sources/SpeziDevices/Health/WeightMeasurement+HKSample.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)
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 Sources/SpeziDevices/Health/WeightMeasurement.Unit+HKUnit.swift
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()
}
}
}
15 changes: 15 additions & 0 deletions Sources/SpeziDevices/Measurements/HealthMeasurement.swift
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)

Check warning on line 14 in Sources/SpeziDevices/Measurements/HealthMeasurement.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

Missing Docs Violation: public declarations should be documented (missing_docs)
}
121 changes: 121 additions & 0 deletions Sources/SpeziDevices/Measurements/HealthMeasurements.swift
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
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
}
Loading

0 comments on commit 4ac6757

Please sign in to comment.