generated from StanfordBDHG/SwiftPackageTemplate
-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add PLX (Pulse Oximeter) support (#54)
# Add PLX (Pulse Oximeter) support ## ♻️ Current situation & Problem Bluetooth defines a [Pulse Oximeter Service](https://www.bluetooth.com/specifications/specs/plxs-html/), which currently is not supported by SpeziBluetoothServices. This PR adds support for this service. ## ⚙️ Release Notes - Added `PulseOximeterService` and related data types (e.g., `PLXContinuousMeasurement`). ## 📚 Documentation The code is documented, similar to the other services that are already defined. ## ✅ Testing The code is tested, similar to the other services that are already defined. ## 📝 Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
- Loading branch information
1 parent
22e7dcf
commit e2600a2
Showing
10 changed files
with
642 additions
and
2 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
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
244 changes: 244 additions & 0 deletions
244
Sources/SpeziBluetoothServices/Characteristics/PLXContinuousMeasurement.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,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) | ||
} | ||
} | ||
} |
117 changes: 117 additions & 0 deletions
117
Sources/SpeziBluetoothServices/Characteristics/PLXFeatures.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,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) | ||
} | ||
} | ||
} |
Oops, something went wrong.