diff --git a/Sources/SpeziBluetoothServices/BluetoothServices.docc/BluetoothServices.md b/Sources/SpeziBluetoothServices/BluetoothServices.docc/BluetoothServices.md index 059bf8c5..5b3fc0b6 100644 --- a/Sources/SpeziBluetoothServices/BluetoothServices.docc/BluetoothServices.md +++ b/Sources/SpeziBluetoothServices/BluetoothServices.docc/BluetoothServices.md @@ -21,5 +21,5 @@ with standardized services and characteristics. ### Articles -- - +- diff --git a/Sources/SpeziBluetoothServices/BluetoothServices.docc/Characteristics.md b/Sources/SpeziBluetoothServices/BluetoothServices.docc/Characteristics.md index 79bb8ee3..355e9de6 100644 --- a/Sources/SpeziBluetoothServices/BluetoothServices.docc/Characteristics.md +++ b/Sources/SpeziBluetoothServices/BluetoothServices.docc/Characteristics.md @@ -37,6 +37,12 @@ which natively integrates with SpeziBluetooth-defined services. - ``ExactTime256`` - ``CurrentTime`` +### Pulse Oximetry + +- ``PLXContinuousMeasurement`` +- ``PLXSpotCheckMeasurement`` +- ``PLXFeatures`` + ### Blood Pressure - ``BloodPressureMeasurement`` diff --git a/Sources/SpeziBluetoothServices/BluetoothServices.docc/Services.md b/Sources/SpeziBluetoothServices/BluetoothServices.docc/Services.md index 1256c8bd..af059e32 100644 --- a/Sources/SpeziBluetoothServices/BluetoothServices.docc/Services.md +++ b/Sources/SpeziBluetoothServices/BluetoothServices.docc/Services.md @@ -29,3 +29,4 @@ Below is a list of reusable Bluetooth service implementations of standardized Bl - ``BloodPressureService`` - ``HealthThermometerService`` - ``WeightScaleService`` +- ``PulseOximeterService`` diff --git a/Sources/SpeziBluetoothServices/Characteristics/PLXContinuousMeasurement.swift b/Sources/SpeziBluetoothServices/Characteristics/PLXContinuousMeasurement.swift new file mode 100644 index 00000000..92d69ae8 --- /dev/null +++ b/Sources/SpeziBluetoothServices/Characteristics/PLXContinuousMeasurement.swift @@ -0,0 +1,244 @@ +// +// 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 ByteCoding +import Foundation +import NIOCore +import SpeziNumerics + + +/// A PLX Continuous Measurement, as defined in the [Bluetooth specification](https://www.bluetooth.com/specifications/specs/plxs-html/). +public struct PLXContinuousMeasurement: ByteCodable, Hashable, Sendable, Codable { + struct Flags: OptionSet, ByteCodable, Sendable, Codable, Hashable { + let rawValue: UInt8 + + static let hasSpO2PRFast = Self(rawValue: 1 << 0) + static let hasSpO2PRSlow = Self(rawValue: 1 << 1) + static let hasMeasurementStatus = Self(rawValue: 1 << 2) + static let hasDeviceAndSensorStatus = Self(rawValue: 1 << 3) + static let hasPulseAmplitudeIndex = Self(rawValue: 1 << 4) + } + + /// Information about the status of the measurement. + public struct MeasurementStatus: OptionSet, PrimitiveByteCodable, Sendable, Codable, Hashable { + public let rawValue: UInt16 + public init(rawValue: UInt16) { + self.rawValue = rawValue + } + + public static let measurementIsOngoing = Self(rawValue: 1 << 5) + public static let earlyEstimatedData = Self(rawValue: 1 << 6) + public static let validatedData = Self(rawValue: 1 << 7) + public static let fullyQualifiedData = Self(rawValue: 1 << 8) + public static let dataFromMeasurementStorage = Self(rawValue: 1 << 9) + public static let dataForDemonstration = Self(rawValue: 1 << 10) + public static let dataForTesting = Self(rawValue: 1 << 11) + public static let calibrationOngoing = Self(rawValue: 1 << 12) + public static let measurementUnavailable = Self(rawValue: 1 << 13) + public static let questionableMeasurementDetected = Self(rawValue: 1 << 14) + public static let invalidMeasurementDetected = Self(rawValue: 1 << 15) + } + + /// Device-level information about the sensor. + public struct DeviceAndSensorStatus: OptionSet, ByteCodable, Sendable, Codable, Hashable { + public let rawValue: UInt32 + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public static let extendedDisplayUpdateOngoing = Self(rawValue: 1 << 0) + public static let equipmentMalfunctionDetected = Self(rawValue: 1 << 1) + public static let signalProcessingIrregularityDetected = Self(rawValue: 1 << 2) + public static let inadequateSignalDetected = Self(rawValue: 1 << 3) + public static let poorSignalDetected = Self(rawValue: 1 << 4) + public static let lowPerfusionDetected = Self(rawValue: 1 << 5) + public static let erraticSignalDetected = Self(rawValue: 1 << 6) + public static let nonpulsatileSignalDetected = Self(rawValue: 1 << 7) + public static let questionablePulseDetected = Self(rawValue: 1 << 8) + public static let signalAnalysisOngoing = Self(rawValue: 1 << 9) + public static let sensorInterferenceDetected = Self(rawValue: 1 << 10) + public static let sensorUnconnectedToUser = Self(rawValue: 1 << 11) + public static let unknownSensorConnected = Self(rawValue: 1 << 12) + public static let sensorDisplaced = Self(rawValue: 1 << 13) + public static let sensorMalfunctioning = Self(rawValue: 1 << 14) + public static let sensorDisconnected = Self(rawValue: 1 << 15) + public static let reservedForFutureUse = Self(rawValue: 0xFFFF0000) + + public init?(from byteBuffer: inout ByteBuffer) { + guard let bytes = byteBuffer.readBytes(length: 3) else { + return nil + } + let rawValue: UInt32 = (UInt32(bytes[2]) << 16) | (UInt32(bytes[1]) << 8) | UInt32(bytes[0]) + self.init(rawValue: rawValue) + } + + public func encode(to byteBuffer: inout ByteBuffer) { + byteBuffer.writeBytes([ + UInt8(truncatingIfNeeded: rawValue), + UInt8(truncatingIfNeeded: rawValue >> 8), + UInt8(truncatingIfNeeded: rawValue >> 16) + ]) + } + } + + + /// The measured blood oxygen saturation + /// Unit: percentage, with a resolution of 1. + public let oxygenSaturation: MedFloat16 + /// The measured heart rate, in beats per minute. + public let pulseRate: MedFloat16 + + /// If available, the fast responding oximetry measurements of the sensor. + /// Unit: percentage, with a resolution of 1. + public let oxygenSaturationFast: MedFloat16? + /// If available, the fast responding oximetry measurements of the sensor. + /// The measured heart rate, in beats per minute. + public let pulseRateFast: MedFloat16? + + /// If available, the slow responding oximetry measurements of the sensor. + /// The measured heart rate, in beats per minute. + public let oxygenSaturationSlow: MedFloat16? + /// If available, the slow responding oximetry measurements of the sensor. + /// The measured heart rate, in beats per minute. + public let pulseRateSlow: MedFloat16? + + /// Measurement status flags. + public let measurementStatus: MeasurementStatus? + /// Device status flags. + public let deviceAndSensorStatus: DeviceAndSensorStatus? + /// Percentage indicating a user's perfusion level (amount of blood being delivered to the capillary bed). + public let pulseAmplitudeIndex: MedFloat16? + + + /// Creates a new ``PLXContinuousMeasurement`` with the specified values. + public init( + oxygenSaturation: MedFloat16, + pulseRate: MedFloat16, + oxygenSaturationFast: MedFloat16? = nil, + pulseRateFast: MedFloat16? = nil, + oxygenSaturationSlow: MedFloat16? = nil, + pulseRateSlow: MedFloat16? = nil, + measurementStatus: MeasurementStatus? = nil, + deviceAndSensorStatus: DeviceAndSensorStatus? = nil, + pulseAmplitudeIndex: MedFloat16? = nil + ) { + self.oxygenSaturation = oxygenSaturation + self.pulseRate = pulseRate + self.oxygenSaturationFast = oxygenSaturationFast + self.pulseRateFast = pulseRateFast + self.oxygenSaturationSlow = oxygenSaturationSlow + self.pulseRateSlow = pulseRateSlow + self.measurementStatus = measurementStatus + self.deviceAndSensorStatus = deviceAndSensorStatus + self.pulseAmplitudeIndex = pulseAmplitudeIndex + } + + + public init?(from byteBuffer: inout NIOCore.ByteBuffer) { // swiftlint:disable:this function_body_length cyclomatic_complexity + guard let flags = Flags(from: &byteBuffer) else { + return nil + } + if let oxygen = MedFloat16(from: &byteBuffer), + let pulse = MedFloat16(from: &byteBuffer) { + self.oxygenSaturation = oxygen + self.pulseRate = pulse + } else { + return nil + } + if flags.contains(.hasSpO2PRFast) { + guard let oxygen = MedFloat16(from: &byteBuffer), + let pulseRate = MedFloat16(from: &byteBuffer) else { + return nil + } + self.oxygenSaturationFast = oxygen + self.pulseRateFast = pulseRate + } else { + self.oxygenSaturationFast = nil + self.pulseRateFast = nil + } + if flags.contains(.hasSpO2PRSlow) { + guard let oxygen = MedFloat16(from: &byteBuffer), + let pulseRate = MedFloat16(from: &byteBuffer) else { + return nil + } + self.oxygenSaturationSlow = oxygen + self.pulseRateSlow = pulseRate + } else { + self.oxygenSaturationSlow = nil + self.pulseRateSlow = nil + } + if flags.contains(.hasMeasurementStatus) { + guard let status = MeasurementStatus(from: &byteBuffer) else { + return nil + } + self.measurementStatus = status + } else { + self.measurementStatus = nil + } + if flags.contains(.hasDeviceAndSensorStatus) { + guard let status = DeviceAndSensorStatus(from: &byteBuffer) else { + return nil + } + self.deviceAndSensorStatus = status + } else { + self.deviceAndSensorStatus = nil + } + if flags.contains(.hasPulseAmplitudeIndex) { + guard let index = MedFloat16(from: &byteBuffer) else { + return nil + } + self.pulseAmplitudeIndex = index + } else { + self.pulseAmplitudeIndex = nil + } + } + + + public func encode(to byteBuffer: inout ByteBuffer) { + let flags: Flags = { + var flags = Flags() + if oxygenSaturationFast != nil, pulseRateFast != nil { + flags.insert(.hasSpO2PRFast) + } + if oxygenSaturationSlow != nil, pulseRateSlow != nil { + flags.insert(.hasSpO2PRSlow) + } + if measurementStatus != nil { + flags.insert(.hasMeasurementStatus) + } + if deviceAndSensorStatus != nil { + flags.insert(.hasDeviceAndSensorStatus) + } + if pulseAmplitudeIndex != nil { + flags.insert(.hasPulseAmplitudeIndex) + } + return flags + }() + flags.encode(to: &byteBuffer) + oxygenSaturation.encode(to: &byteBuffer) + pulseRate.encode(to: &byteBuffer) + + if let oxygenSaturationFast, let pulseRateFast { + oxygenSaturationFast.encode(to: &byteBuffer) + pulseRateFast.encode(to: &byteBuffer) + } + if let oxygenSaturationSlow, let pulseRateSlow { + oxygenSaturationSlow.encode(to: &byteBuffer) + pulseRateSlow.encode(to: &byteBuffer) + } + if let measurementStatus { + measurementStatus.encode(to: &byteBuffer) + } + if let deviceAndSensorStatus { + deviceAndSensorStatus.encode(to: &byteBuffer) + } + if let pulseAmplitudeIndex { + pulseAmplitudeIndex.encode(to: &byteBuffer) + } + } +} diff --git a/Sources/SpeziBluetoothServices/Characteristics/PLXFeatures.swift b/Sources/SpeziBluetoothServices/Characteristics/PLXFeatures.swift new file mode 100644 index 00000000..c486f79d --- /dev/null +++ b/Sources/SpeziBluetoothServices/Characteristics/PLXFeatures.swift @@ -0,0 +1,117 @@ +// +// 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 ByteCoding +import Foundation +import NIOCore +import SpeziNumerics + + +/// The features supported by a Bluetooth-enabled Pulse Oximeter. +public struct PLXFeatures: ByteCodable, Hashable, Sendable, Codable { + /// The measurement-status-fields supported by this device. + /// - Note: Even though this is the same type as ``PLXContinuousMeasurement.MeasurementStatus`` + /// used for continuous measurements, in the ``PLXFeatures``-context, the fields here are interpreted as indicating + /// whether the device supports the field, and not whether the field's described thing is currently true. + /// E.g.: `MeasurementStatus.measurementIsOngoing` being present on `PLXFeatures.measurementStatusSupport` + /// only means that the device can report, when taking a measurement, whether the measurement is still ongoing. + /// It does not mean that there is a measurement ongoing right now. + public typealias MeasurementStatusSupport = PLXContinuousMeasurement.MeasurementStatus + /// The device- and sensor-status-fields supported by this device. + /// - Note: Even though this is the same type as ``PLXContinuousMeasurement.DeviceAndSensorStatus`` + /// used for continuous measurements, in the ``PLXFeatures``-context, the fields here are interpreted as indicating + /// whether the device supports the field, and not whether the field's described thing is currently true. + /// E.g.: `DeviceAndSensorStatus.erraticSignalDetected` being present on `PLXFeatures.deviceAndSensorStatusSupport` + /// only means that the device can report, when taking a measurement, whether an erratic signal was detected. + /// It does not mean that there is an erratic signal right now. + public typealias DeviceAndSensorStatusSupport = PLXContinuousMeasurement.DeviceAndSensorStatus + + /// The features supported by the device. + public struct SupportedFeatures: OptionSet, ByteCodable, Hashable, Sendable, Codable { + public let rawValue: UInt16 + public init(rawValue: UInt16) { + self.rawValue = rawValue + } + + public init?(from byteBuffer: inout ByteBuffer) { + guard let rawValue = byteBuffer.readInteger(endianness: .little, as: RawValue.self) else { + return nil + } + self.init(rawValue: rawValue) + } + + public func encode(to byteBuffer: inout ByteBuffer) { + byteBuffer.writeInteger(rawValue, endianness: .little) + } + + public static let hasMeasurementStatusSupport = Self(rawValue: 1 << 0) + public static let hasDeviceAndSensorStatusSupport = Self(rawValue: 1 << 1) + public static let hasSpotCheckMeasurementsStorage = Self(rawValue: 1 << 2) + public static let hasSpotCheckTimestampSupport = Self(rawValue: 1 << 3) + public static let hasSpO2PRFastSupport = Self(rawValue: 1 << 4) + public static let hasSpO2PRSlowSupport = Self(rawValue: 1 << 5) + public static let hasPulseAmplitudeIndexSupport = Self(rawValue: 1 << 6) + public static let supportsMultipleBonds = Self(rawValue: 1 << 7) + } + + + public let supportedFeatures: SupportedFeatures + public let measurementStatusSupport: MeasurementStatusSupport? + public let deviceAndSensorStatusSupport: DeviceAndSensorStatusSupport? + + internal init( + supportedFeatures: SupportedFeatures, + measurementStatusSupport: MeasurementStatusSupport?, + deviceAndSensorStatusSupport: DeviceAndSensorStatusSupport? + ) { + if supportedFeatures.contains(.hasMeasurementStatusSupport) && measurementStatusSupport == nil { + preconditionFailure(".hasMeasurementStatusSupport flag specified in features, but measurementStatusSupport parameter is nil") + } + if supportedFeatures.contains(.hasDeviceAndSensorStatusSupport) && deviceAndSensorStatusSupport == nil { + preconditionFailure(".hasDeviceAndSensorStatusSupport flag specified in features, but deviceAndSensorStatusSupport parameter is nil") + } + self.supportedFeatures = supportedFeatures + self.measurementStatusSupport = measurementStatusSupport + self.deviceAndSensorStatusSupport = deviceAndSensorStatusSupport + } + + + public init?(from byteBuffer: inout ByteBuffer) { + guard let supportedFeatures = SupportedFeatures(from: &byteBuffer) else { + return nil + } + self.supportedFeatures = supportedFeatures + if supportedFeatures.contains(.hasMeasurementStatusSupport) { + guard let support = MeasurementStatusSupport(from: &byteBuffer) else { + return nil + } + self.measurementStatusSupport = support + } else { + self.measurementStatusSupport = nil + } + if supportedFeatures.contains(.hasDeviceAndSensorStatusSupport) { + guard let support = DeviceAndSensorStatusSupport(from: &byteBuffer) else { + return nil + } + self.deviceAndSensorStatusSupport = support + } else { + self.deviceAndSensorStatusSupport = nil + } + } + + + public func encode(to byteBuffer: inout ByteBuffer) { + supportedFeatures.encode(to: &byteBuffer) + if let measurementStatusSupport { + measurementStatusSupport.encode(to: &byteBuffer) + } + if let deviceAndSensorStatusSupport { + deviceAndSensorStatusSupport.encode(to: &byteBuffer) + } + } +} diff --git a/Sources/SpeziBluetoothServices/Characteristics/PLXSpotCheckMeasurement.swift b/Sources/SpeziBluetoothServices/Characteristics/PLXSpotCheckMeasurement.swift new file mode 100644 index 00000000..310c4cb8 --- /dev/null +++ b/Sources/SpeziBluetoothServices/Characteristics/PLXSpotCheckMeasurement.swift @@ -0,0 +1,143 @@ +// +// 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 ByteCoding +import Foundation +import NIOCore +import SpeziNumerics + + +/// A PLX Spot-check Measurement, as defined in the [Bluetooth specification](https://www.bluetooth.com/specifications/specs/plxs-html/). +public struct PLXSpotCheckMeasurement: ByteCodable, Hashable, Sendable, Codable { + public typealias MeasurementStatus = PLXContinuousMeasurement.MeasurementStatus + public typealias DeviceAndSensorStatus = PLXContinuousMeasurement.DeviceAndSensorStatus + + struct Flags: OptionSet, ByteCodable, Hashable, Codable { + let rawValue: UInt8 + init(rawValue: UInt8) { + self.rawValue = rawValue + } + static let hasTimestamp = Self(rawValue: 1 << 0) + static let hasMeasurementStatus = Self(rawValue: 1 << 1) + static let hasDeviceAndSensorStatus = Self(rawValue: 1 << 2) + static let hasPulseAmplitudeIndex = Self(rawValue: 1 << 3) + static let deviceClockIsNotSet = Self(rawValue: 1 << 4) + } + + /// The measured blood oxygen saturation + /// Unit: percentage, with a resolution of 1. + public let oxygenSaturation: MedFloat16 + /// The measured heart rate, in beats per minute. + public let pulseRate: MedFloat16 + /// The specific time and date the measurement was recorded. + public let timestamp: DateTime? + /// Additional information about the measurement, if applicable. + public let measurementStatus: MeasurementStatus? + /// Device status flags. + public let deviceAndSensorStatus: DeviceAndSensorStatus? + /// Percentage indicating a user's perfusion level (amount of blood being delivered to the capillary bed). + public let pulseAmplitudeIndex: MedFloat16? + + /// Creates a new ``PLXSpotCheckMeasurement`` with the specified values. + public init( + oxygenSaturation: MedFloat16, + pulseRate: MedFloat16, + timestamp: DateTime? = nil, + measurementStatus: MeasurementStatus? = nil, + deviceAndSensorStatus: DeviceAndSensorStatus? = nil, + pulseAmplitudeIndex: MedFloat16? = nil + ) { + self.oxygenSaturation = oxygenSaturation + self.pulseRate = pulseRate + self.timestamp = timestamp + self.measurementStatus = measurementStatus + self.deviceAndSensorStatus = deviceAndSensorStatus + self.pulseAmplitudeIndex = pulseAmplitudeIndex + } + + + public init?(from byteBuffer: inout ByteBuffer) { + guard let flags = Flags(from: &byteBuffer) else { + return nil + } + if let oxygen = MedFloat16(from: &byteBuffer), + let pulseRate = MedFloat16(from: &byteBuffer) { + self.oxygenSaturation = oxygen + self.pulseRate = pulseRate + } else { + return nil + } + if flags.contains(.hasTimestamp) { + guard let timestamp = DateTime(from: &byteBuffer) else { + return nil + } + self.timestamp = timestamp + } else { + self.timestamp = nil + } + if flags.contains(.hasMeasurementStatus) { + guard let status = MeasurementStatus(from: &byteBuffer) else { + return nil + } + self.measurementStatus = status + } else { + self.measurementStatus = nil + } + if flags.contains(.hasDeviceAndSensorStatus) { + guard let status = DeviceAndSensorStatus(from: &byteBuffer) else { + return nil + } + self.deviceAndSensorStatus = status + } else { + self.deviceAndSensorStatus = nil + } + if flags.contains(.hasPulseAmplitudeIndex) { + guard let index = MedFloat16(from: &byteBuffer) else { + return nil + } + self.pulseAmplitudeIndex = index + } else { + self.pulseAmplitudeIndex = nil + } + } + + + public func encode(to byteBuffer: inout ByteBuffer) { + let flags: Flags = { + var flags = Flags() + if timestamp != nil { + flags.insert(.hasTimestamp) + } + if measurementStatus != nil { + flags.insert(.hasMeasurementStatus) + } + if deviceAndSensorStatus != nil { + flags.insert(.hasDeviceAndSensorStatus) + } + if pulseAmplitudeIndex != nil { + flags.insert(.hasPulseAmplitudeIndex) + } + return flags + }() + flags.encode(to: &byteBuffer) + oxygenSaturation.encode(to: &byteBuffer) + pulseRate.encode(to: &byteBuffer) + if let timestamp { + timestamp.encode(to: &byteBuffer) + } + if let measurementStatus { + measurementStatus.encode(to: &byteBuffer) + } + if let deviceAndSensorStatus { + deviceAndSensorStatus.encode(to: &byteBuffer) + } + if let pulseAmplitudeIndex { + pulseAmplitudeIndex.encode(to: &byteBuffer) + } + } +} diff --git a/Sources/SpeziBluetoothServices/Services/PulseOximeterService.swift b/Sources/SpeziBluetoothServices/Services/PulseOximeterService.swift new file mode 100644 index 00000000..1cca1ae5 --- /dev/null +++ b/Sources/SpeziBluetoothServices/Services/PulseOximeterService.swift @@ -0,0 +1,38 @@ +// +// 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 ByteCoding +import Foundation +import NIOCore +import SpeziBluetooth +import SpeziNumerics + + +/// Bluetooth Pulse Oximeter (PLX) Service implementation. +/// +/// This type implements the Bluetooth [Pulse Oximeter Service 1.0.1](https://www.bluetooth.com/specifications/specs/plxs-html/). +public struct PulseOximeterService: BluetoothService, Sendable { + public static let id: BTUUID = "1822" + + /// Defines the features suppored by the pulse oximeter. + /// + /// - Note: This characteristic is required and read-only. + @Characteristic(id: "2A60") + public var features: PLXFeatures? + + /// Read a (usually one-time) spot-check measurement. + @Characteristic(id: "2A5E", notify: true, autoRead: false) + public var spotCheckMeasurement: PLXSpotCheckMeasurement? + + /// Receive continuous PLX (i.e., bood oxygen saturation and pulse rate) measurements. + @Characteristic(id: "2A5F", notify: true, autoRead: false) + public var continuousMeasurement: PLXContinuousMeasurement? + + /// Create a new Pulse Oximeter Service. + public init() {} +} diff --git a/Sources/SpeziBluetoothServices/Services/WeightScaleService.swift b/Sources/SpeziBluetoothServices/Services/WeightScaleService.swift index eaa45ab9..66f6aaaf 100644 --- a/Sources/SpeziBluetoothServices/Services/WeightScaleService.swift +++ b/Sources/SpeziBluetoothServices/Services/WeightScaleService.swift @@ -11,7 +11,7 @@ import SpeziBluetooth /// Bluetooth Weight Scale Service implementation. /// -/// This class implements the Bluetooth [Weight Scale Service 1.0](https://www.bluetooth.com/specifications/specs/weight-scale-service-1-0). +/// This type implements the Bluetooth [Weight Scale Service 1.0](https://www.bluetooth.com/specifications/specs/weight-scale-service-1-0). public struct WeightScaleService: BluetoothService, Sendable { public static let id: BTUUID = "181D" diff --git a/Tests/SpeziBluetoothServicesTests/BluetoothServicesTests.swift b/Tests/SpeziBluetoothServicesTests/BluetoothServicesTests.swift index d62b2f2a..c932197a 100644 --- a/Tests/SpeziBluetoothServicesTests/BluetoothServicesTests.swift +++ b/Tests/SpeziBluetoothServicesTests/BluetoothServicesTests.swift @@ -24,6 +24,7 @@ final class BluetoothServicesTests: XCTestCase { _ = BloodPressureService() _ = BatteryService() _ = CurrentTimeService() + _ = PulseOximeterService() } func testUUID() { diff --git a/Tests/SpeziBluetoothServicesTests/PLXTests.swift b/Tests/SpeziBluetoothServicesTests/PLXTests.swift new file mode 100644 index 00000000..d9c59899 --- /dev/null +++ b/Tests/SpeziBluetoothServicesTests/PLXTests.swift @@ -0,0 +1,90 @@ +// +// 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 NIO +import SpeziNumerics +@_spi(TestingSupport) +@testable import SpeziBluetooth +@_spi(TestingSupport) +@testable import SpeziBluetoothServices +import XCTByteCoding +import XCTest + + +final class PLXTests: XCTestCase { + func testVerySimpleMeasurementCoding() throws { + let measurement = PLXContinuousMeasurement(oxygenSaturation: 100, pulseRate: 90) + try testIdentity(from: measurement) + XCTAssertEqual(measurement.encode(), Data([0, 232, 243, 132, 243])) + XCTAssertEqual(PLXContinuousMeasurement(data: Data([0, 100, 0, 90, 0])), measurement) + } + + func testSimpleMeasurementCoding() throws { + let data = Data([28, 95, 0, 90, 0, 32, 0, 0, 0, 0, 87, 240]) + let measurement = try XCTUnwrap(PLXContinuousMeasurement(data: data)) + XCTAssertEqual(PLXContinuousMeasurement(data: measurement.encode()), measurement) + XCTAssertEqual(measurement, PLXContinuousMeasurement( + oxygenSaturation: 95, + pulseRate: 90, + measurementStatus: .measurementIsOngoing, + deviceAndSensorStatus: [], + pulseAmplitudeIndex: 87e-1 + )) + try testIdentity(from: measurement) + XCTAssertEqual(measurement.encode(), data) + XCTAssertEqual(PLXContinuousMeasurement(data: data), measurement) + } + + + func testFullMeasurementCoding() throws { + let measurement = PLXContinuousMeasurement( + oxygenSaturation: 99, + pulseRate: 97, + oxygenSaturationFast: 97, + pulseRateFast: 137, + oxygenSaturationSlow: 98, + pulseRateSlow: 69, + measurementStatus: [.measurementIsOngoing, .fullyQualifiedData], + deviceAndSensorStatus: [], + pulseAmplitudeIndex: 89e-1 + ) + try testIdentity(from: measurement) + let data = Data([31, 222, 243, 202, 243, 202, 243, 90, 245, 212, 243, 178, 242, 32, 1, 0, 0, 0, 122, 227]) + XCTAssertEqual(measurement.encode(), data) + XCTAssertEqual(PLXContinuousMeasurement(data: data), measurement) + } + + + func testSpotCheckMeasurementCoding() throws { + let measurement = PLXSpotCheckMeasurement( + oxygenSaturation: 94, + pulseRate: 147, + timestamp: DateTime(year: 2024, month: .november, day: 29, hours: 23, minutes: 08, seconds: 57), + measurementStatus: [.measurementIsOngoing, .questionableMeasurementDetected], + deviceAndSensorStatus: [.equipmentMalfunctionDetected, .lowPerfusionDetected], + pulseAmplitudeIndex: 87e-1 + ) + try testIdentity(from: measurement) + let data = Data([15, 172, 243, 190, 245, 232, 7, 11, 29, 23, 8, 57, 32, 64, 34, 0, 0, 102, 227]) + XCTAssertEqual(measurement.encode(), data) + XCTAssertEqual(PLXSpotCheckMeasurement(data: data), measurement) + } + + + func testPLXFeaturesCoding() throws { + let features = PLXFeatures( + supportedFeatures: [.hasMeasurementStatusSupport, .hasDeviceAndSensorStatusSupport, .hasSpO2PRSlowSupport, .supportsMultipleBonds], + measurementStatusSupport: [.calibrationOngoing, .earlyEstimatedData], + deviceAndSensorStatusSupport: [.nonpulsatileSignalDetected, .questionablePulseDetected] + ) + try testIdentity(from: features) + let data = Data([163, 0, 64, 16, 128, 1, 0]) + XCTAssertEqual(features.encode(), data) + XCTAssertEqual(PLXFeatures(data: data), features) + } +}