Skip to content

Commit

Permalink
Add PLX (Pulse Oximeter) support (#54)
Browse files Browse the repository at this point in the history
# 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
lukaskollmer authored Nov 30, 2024
1 parent 22e7dcf commit e2600a2
Show file tree
Hide file tree
Showing 10 changed files with 642 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ with standardized services and characteristics.

### Articles

- <doc:Characteristics>
- <doc:Services>
- <doc:Characteristics>
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ which natively integrates with SpeziBluetooth-defined services.
- ``ExactTime256``
- ``CurrentTime``

### Pulse Oximetry

- ``PLXContinuousMeasurement``
- ``PLXSpotCheckMeasurement``
- ``PLXFeatures``

### Blood Pressure

- ``BloodPressureMeasurement``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ Below is a list of reusable Bluetooth service implementations of standardized Bl
- ``BloodPressureService``
- ``HealthThermometerService``
- ``WeightScaleService``
- ``PulseOximeterService``
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 Sources/SpeziBluetoothServices/Characteristics/PLXFeatures.swift
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)
}
}
}
Loading

0 comments on commit e2600a2

Please sign in to comment.