[O] Refactor notification system

This commit is contained in:
Azalea Gui
2023-01-23 17:41:12 -05:00
parent c487c917da
commit be20c663b0
21 changed files with 512 additions and 507 deletions
@@ -1,52 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SFLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import java.nio.ByteOrder
import java.util.*
data class BloodPressureMeasurement(
val systolic: Float,
val diastolic: Float,
val meanArterialPressure: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val pulseRate: Float?,
val userID: Int?,
val measurementStatus: BloodPressureMeasurementStatus?,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): BloodPressureMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) ObservationUnit.MMHG else ObservationUnit.KPA
val timestampPresent = flags and 0x02 > 0
val pulseRatePresent = flags and 0x04 > 0
val userIdPresent = flags and 0x08 > 0
val measurementStatusPresent = flags and 0x10 > 0
val systolic = parser.getFloatValue(FORMAT_SFLOAT)
val diastolic = parser.getFloatValue(FORMAT_SFLOAT)
val meanArterialPressure = parser.getFloatValue(FORMAT_SFLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val pulseRate = if (pulseRatePresent) parser.getFloatValue(FORMAT_SFLOAT) else null
val userID = if (userIdPresent) parser.getIntValue(FORMAT_UINT8) else null
val status = if (measurementStatusPresent) BloodPressureMeasurementStatus(parser.getIntValue(FORMAT_UINT16)) else null
return BloodPressureMeasurement(
systolic = systolic,
diastolic = diastolic,
meanArterialPressure = meanArterialPressure,
unit = unit,
timestamp = timestamp,
pulseRate = pulseRate,
userID = userID,
measurementStatus = status
)
}
}
}
@@ -1,32 +0,0 @@
package org.hydev.wearsync.bles
class BloodPressureMeasurementStatus internal constructor(measurementStatus: Int) {
/**
* Body Movement Detected
*/
val isBodyMovementDetected: Boolean
/**
* Cuff is too loose
*/
val isCuffTooLoose: Boolean
/**
* Irregular pulse detected
*/
val isIrregularPulseDetected: Boolean
/**
* Pulse is not in normal range
*/
val isPulseNotInRange: Boolean
/**
* Improper measurement position
*/
val isImproperMeasurementPosition: Boolean
init {
isBodyMovementDetected = measurementStatus and 0x0001 > 0
isCuffTooLoose = measurementStatus and 0x0002 > 0
isIrregularPulseDetected = measurementStatus and 0x0004 > 0
isPulseNotInRange = measurementStatus and 0x0008 > 0
isImproperMeasurementPosition = measurementStatus and 0x0020 > 0
}
}
@@ -5,6 +5,7 @@ import com.welie.blessed.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import org.hydev.wearsync.bles.decoders.*
import timber.log.Timber
import timber.log.Timber.DebugTree
import java.nio.ByteOrder
@@ -13,6 +14,7 @@ import java.util.*
internal class BluetoothHandler private constructor(context: Context) {
private var currentTimeCounter = 0
val batteryChannel = Channel<UInt>(UNLIMITED)
val heartRateChannel = Channel<HeartRateMeasurement>(UNLIMITED)
val bloodPressureChannel = Channel<BloodPressureMeasurement>(UNLIMITED)
val glucoseChannel = Channel<GlucoseMeasurement>(UNLIMITED)
@@ -24,24 +26,12 @@ internal class BluetoothHandler private constructor(context: Context) {
private fun handlePeripheral(peripheral: BluetoothPeripheral) {
scope.launch {
try {
val mtu = peripheral.requestMtu(185)
Timber.i("MTU is $mtu")
Timber.i("MTU is ${peripheral.requestMtu(185)}")
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
val rssi = peripheral.readRemoteRssi()
Timber.i("RSSI is $rssi")
peripheral.getCharacteristic(DIS_SERVICE_UUID, MANUFACTURER_NAME_CHARACTERISTIC_UUID)?.let {
val manufacturerName = peripheral.readCharacteristic(it).asString()
Timber.i("Received: $manufacturerName")
}
val model = peripheral.readCharacteristic(DIS_SERVICE_UUID, MODEL_NUMBER_CHARACTERISTIC_UUID).asString()
Timber.i("Received: $model")
val batteryLevel = peripheral.readCharacteristic(BTS_SERVICE_UUID, BATTERY_LEVEL_CHARACTERISTIC_UUID).asUInt8()
Timber.i("Battery level: $batteryLevel")
Timber.i("RSSI is ${peripheral.readRemoteRssi()}")
Timber.i("Received: ${peripheral.read(ManufacturerDecoder())}")
Timber.i("Received: ${peripheral.read(ModelDecoder())}")
Timber.i("Battery level: ${peripheral.read(BatteryDecoder())}")
// Write Current Time if possible
peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let {
@@ -56,12 +46,14 @@ internal class BluetoothHandler private constructor(context: Context) {
}
}
setupHRSnotifications(peripheral)
setupPLXnotifications(peripheral)
setupHTSnotifications(peripheral)
peripheral.setupNotification(batteryChannel, BatteryDecoder())
peripheral.setupNotification(heartRateChannel, HeartRateDecoder())
peripheral.setupNotification(temperatureChannel, TemperatureDecoder())
peripheral.setupNotification(weightChannel, WeightDecoder())
peripheral.setupNotification(bloodPressureChannel, BloodPressureDecoder())
peripheral.setupNotification(pulseOxSpotChannel, PLXSpotDecoder())
peripheral.setupNotification(pulseOxContinuousChannel, PLXContinuousDecoder())
setupGLXnotifications(peripheral)
setupBLPnotifications(peripheral)
setupWSSnotifications(peripheral)
setupCTSnotifications(peripheral)
peripheral.getCharacteristic(CONTOUR_SERVICE_UUID, CONTOUR_CLOCK)?.let {
@@ -119,34 +111,8 @@ internal class BluetoothHandler private constructor(context: Context) {
}
}
private suspend fun setupHRSnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(HRS_SERVICE_UUID, HRS_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = HeartRateMeasurement.fromBytes(value)
heartRateChannel.trySend(measurement)
Timber.d("%s", measurement)
}
}
}
private suspend fun setupWSSnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(WSS_SERVICE_UUID, WSS_MEASUREMENT_CHAR_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = WeightMeasurement.fromBytes(value)
weightChannel.trySend(measurement)
Timber.d("%s", measurement)
}
}
}
private suspend fun setupGLXnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = GlucoseMeasurement.fromBytes(value)
glucoseChannel.trySend(measurement)
Timber.d("%s", measurement)
}
}
peripheral.setupNotification(glucoseChannel, GlucoseDecoder())
peripheral.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID)?.let {
val result = peripheral.observe(it) { value ->
@@ -166,42 +132,13 @@ internal class BluetoothHandler private constructor(context: Context) {
peripheral.writeCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID, command, WriteType.WITH_RESPONSE)
}
private suspend fun setupBLPnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(BLP_SERVICE_UUID, BLP_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = BloodPressureMeasurement.fromBytes(value)
bloodPressureChannel.trySend(measurement)
Timber.d("%s", measurement)
}
}
}
private suspend fun setupHTSnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(HTS_SERVICE_UUID, HTS_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = TemperatureMeasurement.fromBytes(value)
temperatureChannel.trySend(measurement)
Timber.d("%s", measurement)
}
}
}
private suspend fun setupPLXnotifications(peripheral: BluetoothPeripheral) {
peripheral.getCharacteristic(PLX_SERVICE_UUID, PLX_CONTINUOUS_MEASUREMENT_CHAR_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = PulseOximeterContinuousMeasurement.fromBytes(value)
if (measurement.spO2 <= 100 && measurement.pulseRate <= 220) {
pulseOxContinuousChannel.trySend(measurement)
}
Timber.d("%s", measurement)
}
}
peripheral.getCharacteristic(PLX_SERVICE_UUID, PLX_SPOT_MEASUREMENT_CHAR_UUID)?.let {
peripheral.observe(it) { value ->
val measurement = PulseOximeterSpotMeasurement.fromBytes(value)
pulseOxSpotChannel.trySend(measurement)
Timber.d("%s", measurement)
suspend fun <T> BluetoothPeripheral.setupNotification(channel: Channel<T>, dec: IDecoder<T>)
{
getCharacteristic(dec.sid, dec.cid)?.let {
observe(it) { value ->
val measurement = dec.decode(value)
channel.trySend(measurement)
Timber.d(measurement.toString())
}
}
}
@@ -236,44 +173,18 @@ internal class BluetoothHandler private constructor(context: Context) {
companion object {
// UUIDs for the Blood Pressure service (BLP)
val BLP_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
val BLP_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
// UUIDs for the Health Thermometer service (HTS)
val HTS_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
val HTS_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
// UUIDs for the Heart Rate service (HRS)
val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
val HRS_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
// UUIDs for the Device Information service (DIS)
val DIS_SERVICE_UUID: UUID = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb")
val MANUFACTURER_NAME_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A29-0000-1000-8000-00805f9b34fb")
val MODEL_NUMBER_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A24-0000-1000-8000-00805f9b34fb")
val BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
val BLP_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
// UUIDs for the Current Time service (CTS)
val CTS_SERVICE_UUID: UUID = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb")
val CURRENT_TIME_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A2B-0000-1000-8000-00805f9b34fb")
val CTS_SERVICE_UUID = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb")
val CURRENT_TIME_CHARACTERISTIC_UUID = UUID.fromString("00002A2B-0000-1000-8000-00805f9b34fb")
// UUIDs for the Battery Service (BAS)
val BTS_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
val BATTERY_LEVEL_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
// UUIDs for the Pulse Oximeter Service (PLX)
val PLX_SERVICE_UUID: UUID = UUID.fromString("00001822-0000-1000-8000-00805f9b34fb")
val PLX_SPOT_MEASUREMENT_CHAR_UUID: UUID = UUID.fromString("00002a5e-0000-1000-8000-00805f9b34fb")
val PLX_CONTINUOUS_MEASUREMENT_CHAR_UUID: UUID = UUID.fromString("00002a5f-0000-1000-8000-00805f9b34fb")
// UUIDs for the Weight Scale Service (WSS)
val WSS_SERVICE_UUID: UUID = UUID.fromString("0000181D-0000-1000-8000-00805f9b34fb")
val WSS_MEASUREMENT_CHAR_UUID: UUID = UUID.fromString("00002A9D-0000-1000-8000-00805f9b34fb")
val GLUCOSE_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
val GLUCOSE_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
val GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
val GLUCOSE_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
val GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
// Contour Glucose Service
val CONTOUR_SERVICE_UUID: UUID = UUID.fromString("00000000-0002-11E2-9E96-0800200C9A66")
val CONTOUR_SERVICE_UUID = UUID.fromString("00000000-0002-11E2-9E96-0800200C9A66")
val CONTOUR_CLOCK = UUID.fromString("00001026-0002-11E2-9E96-0800200C9A66")
private var instance: BluetoothHandler? = null
@@ -283,6 +194,9 @@ internal class BluetoothHandler private constructor(context: Context) {
}
return instance!!
}
suspend fun <T> BluetoothPeripheral.read(decoder: IDecoder<T>) =
getCharacteristic(decoder.sid, decoder.cid)?.let { decoder.decode(readCharacteristic(it)) }
}
@JvmField
@@ -1,49 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SFLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import org.hydev.wearsync.bles.ObservationUnit.MiligramPerDeciliter
import org.hydev.wearsync.bles.ObservationUnit.MmolPerLiter
import java.util.*
data class GlucoseMeasurement(
val value: Float?,
val unit: ObservationUnit,
val timestamp: Date?,
val sequenceNumber: Int,
val contextWillFollow: Boolean,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): GlucoseMeasurement
{
val parser = BluetoothBytesParser(value)
val flags: Int = parser.getIntValue(FORMAT_UINT8)
val timeOffsetPresent = flags and 0x01 > 0
val typeAndLocationPresent = flags and 0x02 > 0
val unit = if (flags and 0x04 > 0) MmolPerLiter else MiligramPerDeciliter
val contextWillFollow = flags and 0x10 > 0
val sequenceNumber = parser.getIntValue(FORMAT_UINT16)
var timestamp = parser.dateTime
if (timeOffsetPresent) {
val timeOffset: Int = parser.getIntValue(FORMAT_SINT16)
timestamp = Date(timestamp.time + timeOffset * 60000)
}
val multiplier = if (unit === MiligramPerDeciliter) 100000 else 1000
val glucoseValue = if (typeAndLocationPresent) parser.getFloatValue(FORMAT_SFLOAT) * multiplier else null
return GlucoseMeasurement(
unit = unit,
timestamp = timestamp,
sequenceNumber = sequenceNumber,
value = glucoseValue,
contextWillFollow = contextWillFollow
)
}
}
}
@@ -1,51 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import java.nio.ByteOrder
import java.util.*
data class HeartRateMeasurement(
val pulse: Int,
val energyExpended: Int?,
val rrIntervals: IntArray,
val sensorContactStatus: SensorContactFeature,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): HeartRateMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(FORMAT_UINT8)
val pulse = if (flags and 0x01 == 0) parser.getIntValue(FORMAT_UINT8) else parser.getIntValue(FORMAT_UINT16)
val sensorContactStatusFlag = flags and 0x06 shr 1
val energyExpenditurePresent = flags and 0x08 > 0
val rrIntervalPresent = flags and 0x10 > 0
val sensorContactStatus = when (sensorContactStatusFlag) {
0, 1 -> SensorContactFeature.NotSupported
2 -> SensorContactFeature.SupportedNoContact
3 -> SensorContactFeature.SupportedAndContact
else -> SensorContactFeature.NotSupported
}
val energyExpended = if (energyExpenditurePresent) parser.getIntValue(FORMAT_UINT16) else null
val rrArray = ArrayList<Int>()
if (rrIntervalPresent) {
while (parser.offset < value.size) {
val rrInterval = parser.getIntValue(FORMAT_UINT16)
rrArray.add((rrInterval.toDouble() / 1024.0 * 1000.0).toInt())
}
}
return HeartRateMeasurement(
pulse = pulse,
energyExpended = energyExpended,
sensorContactStatus = sensorContactStatus,
rrIntervals = rrArray.toIntArray()
)
}
}
}
@@ -1,56 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SFLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import java.util.*
data class PulseOximeterContinuousMeasurement(
val spO2: Float,
val pulseRate: Float,
val spO2Fast: Float?,
val pulseRateFast: Float?,
val spO2Slow: Float?,
val pulseRateSlow: Float?,
val pulseAmplitudeIndex: Float?,
val measurementStatus: Int?,
val sensorStatus: Int?,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): PulseOximeterContinuousMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(FORMAT_UINT8)
val spo2FastPresent = flags and 0x01 > 0
val spo2SlowPresent = flags and 0x02 > 0
val measurementStatusPresent = flags and 0x04 > 0
val sensorStatusPresent = flags and 0x08 > 0
val pulseAmplitudeIndexPresent = flags and 0x10 > 0
val spO2 = parser.getFloatValue(FORMAT_SFLOAT)
val pulseRate = parser.getFloatValue(FORMAT_SFLOAT)
val spO2Fast = if (spo2FastPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
val pulseRateFast = if (spo2FastPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
val spO2Slow = if (spo2SlowPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
val pulseRateSlow = if (spo2SlowPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
val measurementStatus = if (measurementStatusPresent) parser.getIntValue(FORMAT_UINT16) else null
val sensorStatus = if (sensorStatusPresent) parser.getIntValue(FORMAT_UINT16) else null
if (sensorStatusPresent) parser.getIntValue(FORMAT_UINT8) // Reserved byte
val pulseAmplitudeIndex = if (pulseAmplitudeIndexPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
return PulseOximeterContinuousMeasurement(
spO2 = spO2,
pulseRate = pulseRate,
spO2Fast = spO2Fast,
pulseRateFast = pulseRateFast,
spO2Slow = spO2Slow,
pulseRateSlow = pulseRateSlow,
measurementStatus = measurementStatus,
sensorStatus = sensorStatus,
pulseAmplitudeIndex = pulseAmplitudeIndex
)
}
}
}
@@ -1,49 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SFLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import java.util.*
data class PulseOximeterSpotMeasurement(
val spO2: Float,
val pulseRate: Float,
val pulseAmplitudeIndex: Float?,
val timestamp: Date?,
val isDeviceClockSet: Boolean,
val measurementStatus: Int?,
val sensorStatus: Int?,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): PulseOximeterSpotMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(FORMAT_UINT8)
val timestampPresent = flags and 0x01 > 0
val measurementStatusPresent = flags and 0x02 > 0
val sensorStatusPresent = flags and 0x04 > 0
val pulseAmplitudeIndexPresent = flags and 0x08 > 0
val isDeviceClockSet = flags and 0x10 == 0
val spO2 = parser.getFloatValue(FORMAT_SFLOAT)
val pulseRate = parser.getFloatValue(FORMAT_SFLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val measurementStatus = if (measurementStatusPresent) parser.getIntValue(FORMAT_UINT16) else null
val sensorStatus = if (sensorStatusPresent) parser.getIntValue(FORMAT_UINT16) else null
if (sensorStatusPresent) parser.getIntValue(FORMAT_UINT8) // Reserved byte
val pulseAmplitudeIndex = if (pulseAmplitudeIndexPresent) parser.getFloatValue(FORMAT_SFLOAT) else null
return PulseOximeterSpotMeasurement(
spO2 = spO2,
pulseRate = pulseRate,
measurementStatus = measurementStatus,
sensorStatus = sensorStatus,
pulseAmplitudeIndex = pulseAmplitudeIndex,
timestamp = timestamp,
isDeviceClockSet = isDeviceClockSet
)
}
}
}
@@ -1,37 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_FLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import java.nio.ByteOrder
import java.util.*
data class TemperatureMeasurement(
val temperatureValue: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val type: TemperatureType,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): TemperatureMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) ObservationUnit.Fahrenheit else ObservationUnit.Celsius
val timestampPresent = flags and 0x02 > 0
val typePresent = flags and 0x04 > 0
val temperatureValue = parser.getFloatValue(FORMAT_FLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val type = if (typePresent) TemperatureType.fromValue(parser.getIntValue(FORMAT_UINT8)) else TemperatureType.Unknown
return TemperatureMeasurement(
unit = unit,
temperatureValue = temperatureValue,
timestamp = timestamp,
type = type
)
}
}
}
@@ -1,15 +0,0 @@
package org.hydev.wearsync.bles
enum class TemperatureType(val value: Int) {
Unknown(0), Armpit(1), Body(2), Ear(3), Finger(4), GastroIntestinalTract(5), Mouth(6), Rectum(7), Toe(8), Tympanum(9);
companion object {
fun fromValue(value: Int): TemperatureType
{
for (type in values()) {
if (type.value == value) return type
}
return Unknown
}
}
}
@@ -1,48 +0,0 @@
package org.hydev.wearsync.bles
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import org.hydev.wearsync.bles.ObservationUnit.Kilograms
import org.hydev.wearsync.bles.ObservationUnit.Pounds
import java.util.*
import kotlin.math.round
data class WeightMeasurement(
val weight: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val userID: Int?,
val bmi: Float?,
val heightInMetersOrInches: Float?,
val createdAt: Date = Calendar.getInstance().time
) {
companion object {
fun fromBytes(value: ByteArray): WeightMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) Pounds else Kilograms
val timestampPresent = flags and 0x02 > 0
val userIDPresent = flags and 0x04 > 0
val bmiAndHeightPresent = flags and 0x08 > 0
val weightMultiplier = if (unit == Kilograms) 0.005f else 0.01f
val weight = parser.getIntValue(FORMAT_UINT16) * weightMultiplier
val timestamp = if (timestampPresent) parser.dateTime else null
val userID = if (userIDPresent) parser.getIntValue(FORMAT_UINT8) else null
val bmi = if (bmiAndHeightPresent) parser.getIntValue(FORMAT_UINT16) * 0.1f else null
val heightMultiplier = if (unit == Kilograms) 0.001f else 0.1f
val height = if (bmiAndHeightPresent) parser.getIntValue(FORMAT_UINT16) * heightMultiplier else null
return WeightMeasurement(
weight = round(weight * 100) / 100,
unit = unit,
timestamp = timestamp,
userID = userID,
bmi = bmi,
heightInMetersOrInches = height
)
}
}
}
@@ -0,0 +1,15 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.asUInt8
import java.util.*
class BatteryDecoder : IDecoder<UInt>
{
override val sid = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): UInt
{
return value.asUInt8()!!
}
}
@@ -0,0 +1,87 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import org.hydev.wearsync.bles.ObservationUnit
import java.nio.ByteOrder
import java.util.*
class BloodPressureStatus internal constructor(measurementStatus: Int) {
/**
* Body Movement Detected
*/
val isBodyMovementDetected: Boolean
/**
* Cuff is too loose
*/
val isCuffTooLoose: Boolean
/**
* Irregular pulse detected
*/
val isIrregularPulseDetected: Boolean
/**
* Pulse is not in normal range
*/
val isPulseNotInRange: Boolean
/**
* Improper measurement position
*/
val isImproperMeasurementPosition: Boolean
init {
isBodyMovementDetected = measurementStatus and 0x0001 > 0
isCuffTooLoose = measurementStatus and 0x0002 > 0
isIrregularPulseDetected = measurementStatus and 0x0004 > 0
isPulseNotInRange = measurementStatus and 0x0008 > 0
isImproperMeasurementPosition = measurementStatus and 0x0020 > 0
}
}
data class BloodPressureMeasurement(
val systolic: Float,
val diastolic: Float,
val meanArterialPressure: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val pulseRate: Float?,
val userID: Int?,
val measurementStatus: BloodPressureStatus?,
val createdAt: Date = Calendar.getInstance().time
)
class BloodPressureDecoder : IDecoder<BloodPressureMeasurement>
{
override val sid = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): BloodPressureMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) ObservationUnit.MMHG else ObservationUnit.KPA
val timestampPresent = flags and 0x02 > 0
val pulseRatePresent = flags and 0x04 > 0
val userIdPresent = flags and 0x08 > 0
val measurementStatusPresent = flags and 0x10 > 0
val systolic = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val diastolic = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val meanArterialPressure = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val pulseRate = if (pulseRatePresent) parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT) else null
val userID = if (userIdPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8) else null
val status = if (measurementStatusPresent) BloodPressureStatus(parser.getIntValue(
BluetoothBytesParser.FORMAT_UINT16
)) else null
return BloodPressureMeasurement(
systolic = systolic,
diastolic = diastolic,
meanArterialPressure = meanArterialPressure,
unit = unit,
timestamp = timestamp,
pulseRate = pulseRate,
userID = userID,
measurementStatus = status
)
}
}
@@ -0,0 +1,55 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SFLOAT
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_SINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import org.hydev.wearsync.bles.BluetoothHandler.Companion.GLUCOSE_SERVICE_UUID
import org.hydev.wearsync.bles.ObservationUnit
import org.hydev.wearsync.bles.ObservationUnit.MiligramPerDeciliter
import org.hydev.wearsync.bles.ObservationUnit.MmolPerLiter
import java.util.*
data class GlucoseMeasurement(
val value: Float?,
val unit: ObservationUnit,
val timestamp: Date?,
val sequenceNumber: Int,
val contextWillFollow: Boolean,
val createdAt: Date = Calendar.getInstance().time
)
class GlucoseDecoder : IDecoder<GlucoseMeasurement>
{
override val sid = GLUCOSE_SERVICE_UUID
override val cid = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): GlucoseMeasurement
{
val parser = BluetoothBytesParser(value)
val flags: Int = parser.getIntValue(FORMAT_UINT8)
val timeOffsetPresent = flags and 0x01 > 0
val typeAndLocationPresent = flags and 0x02 > 0
val unit = if (flags and 0x04 > 0) MmolPerLiter else MiligramPerDeciliter
val contextWillFollow = flags and 0x10 > 0
val sequenceNumber = parser.getIntValue(FORMAT_UINT16)
var timestamp = parser.dateTime
if (timeOffsetPresent) {
val timeOffset: Int = parser.getIntValue(FORMAT_SINT16)
timestamp = Date(timestamp.time + timeOffset * 60000)
}
val multiplier = if (unit === MiligramPerDeciliter) 100000 else 1000
val glucoseValue = if (typeAndLocationPresent) parser.getFloatValue(FORMAT_SFLOAT) * multiplier else null
return GlucoseMeasurement(
unit = unit,
timestamp = timestamp,
sequenceNumber = sequenceNumber,
value = glucoseValue,
contextWillFollow = contextWillFollow
)
}
}
@@ -0,0 +1,56 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT16
import com.welie.blessed.BluetoothBytesParser.Companion.FORMAT_UINT8
import org.hydev.wearsync.bles.SensorContactFeature
import java.nio.ByteOrder
import java.util.*
data class HeartRateMeasurement(
val pulse: Int,
val energyExpended: Int?,
val rrIntervals: IntArray,
val sensorContactStatus: SensorContactFeature,
val createdAt: Date = Calendar.getInstance().time
)
class HeartRateDecoder : IDecoder<HeartRateMeasurement>
{
override val sid = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): HeartRateMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(FORMAT_UINT8)
val pulse = if (flags and 0x01 == 0) parser.getIntValue(FORMAT_UINT8) else parser.getIntValue(FORMAT_UINT16)
val sensorContactStatusFlag = flags and 0x06 shr 1
val energyExpenditurePresent = flags and 0x08 > 0
val rrIntervalPresent = flags and 0x10 > 0
val sensorContactStatus = when (sensorContactStatusFlag) {
0, 1 -> SensorContactFeature.NotSupported
2 -> SensorContactFeature.SupportedNoContact
3 -> SensorContactFeature.SupportedAndContact
else -> SensorContactFeature.NotSupported
}
val energyExpended = if (energyExpenditurePresent) parser.getIntValue(FORMAT_UINT16) else null
val rrArray = ArrayList<Int>()
if (rrIntervalPresent) {
while (parser.offset < value.size) {
val rrInterval = parser.getIntValue(FORMAT_UINT16)
rrArray.add((rrInterval.toDouble() / 1024.0 * 1000.0).toInt())
}
}
return HeartRateMeasurement(
pulse = pulse,
energyExpended = energyExpended,
sensorContactStatus = sensorContactStatus,
rrIntervals = rrArray.toIntArray()
)
}
}
@@ -0,0 +1,11 @@
package org.hydev.wearsync.bles.decoders
import java.util.*
interface IDecoder<T>
{
val sid: UUID
val cid: UUID
fun decode(value: ByteArray): T
}
@@ -0,0 +1,12 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.asString
import java.util.*
class ManufacturerDecoder : IDecoder<String>
{
override val sid = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A29-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray) = value.asString()
}
@@ -0,0 +1,15 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.asString
import java.util.*
class ModelDecoder : IDecoder<String>
{
override val sid = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A24-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): String
{
return value.asString()
}
}
@@ -0,0 +1,61 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import java.util.*
data class PulseOximeterContinuousMeasurement(
val spO2: Float,
val pulseRate: Float,
val spO2Fast: Float?,
val pulseRateFast: Float?,
val spO2Slow: Float?,
val pulseRateSlow: Float?,
val pulseAmplitudeIndex: Float?,
val measurementStatus: Int?,
val sensorStatus: Int?,
val createdAt: Date = Calendar.getInstance().time
)
class PLXContinuousDecoder : IDecoder<PulseOximeterContinuousMeasurement> {
override val sid = UUID.fromString("00001822-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002a5f-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): PulseOximeterContinuousMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8)
val spo2FastPresent = flags and 0x01 > 0
val spo2SlowPresent = flags and 0x02 > 0
val measurementStatusPresent = flags and 0x04 > 0
val sensorStatusPresent = flags and 0x08 > 0
val pulseAmplitudeIndexPresent = flags and 0x10 > 0
val spO2 = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val pulseRate = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val spO2Fast = if (spo2FastPresent) parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT) else null
val pulseRateFast = if (spo2FastPresent) parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT) else null
val spO2Slow = if (spo2SlowPresent) parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT) else null
val pulseRateSlow = if (spo2SlowPresent) parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT) else null
val measurementStatus = if (measurementStatusPresent) parser.getIntValue(
BluetoothBytesParser.FORMAT_UINT16
) else null
val sensorStatus = if (sensorStatusPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) else null
if (sensorStatusPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8) // Reserved byte
val pulseAmplitudeIndex = if (pulseAmplitudeIndexPresent) parser.getFloatValue(
BluetoothBytesParser.FORMAT_SFLOAT
) else null
return PulseOximeterContinuousMeasurement(
spO2 = spO2,
pulseRate = pulseRate,
spO2Fast = spO2Fast,
pulseRateFast = pulseRateFast,
spO2Slow = spO2Slow,
pulseRateSlow = pulseRateSlow,
measurementStatus = measurementStatus,
sensorStatus = sensorStatus,
pulseAmplitudeIndex = pulseAmplitudeIndex
)
}
}
@@ -0,0 +1,57 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import java.util.*
data class PulseOximeterSpotMeasurement(
val spO2: Float,
val pulseRate: Float,
val pulseAmplitudeIndex: Float?,
val timestamp: Date?,
val isDeviceClockSet: Boolean,
val measurementStatus: Int?,
val sensorStatus: Int?,
val createdAt: Date = Calendar.getInstance().time
)
/**
* Pulse Oximeter Service (PLX)
*/
class PLXSpotDecoder : IDecoder<PulseOximeterSpotMeasurement>
{
override val sid = UUID.fromString("00001822-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002a5e-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): PulseOximeterSpotMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8)
val timestampPresent = flags and 0x01 > 0
val measurementStatusPresent = flags and 0x02 > 0
val sensorStatusPresent = flags and 0x04 > 0
val pulseAmplitudeIndexPresent = flags and 0x08 > 0
val isDeviceClockSet = flags and 0x10 == 0
val spO2 = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val pulseRate = parser.getFloatValue(BluetoothBytesParser.FORMAT_SFLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val measurementStatus = if (measurementStatusPresent) parser.getIntValue(
BluetoothBytesParser.FORMAT_UINT16
) else null
val sensorStatus = if (sensorStatusPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) else null
if (sensorStatusPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8) // Reserved byte
val pulseAmplitudeIndex = if (pulseAmplitudeIndexPresent) parser.getFloatValue(
BluetoothBytesParser.FORMAT_SFLOAT
) else null
return PulseOximeterSpotMeasurement(
spO2 = spO2,
pulseRate = pulseRate,
measurementStatus = measurementStatus,
sensorStatus = sensorStatus,
pulseAmplitudeIndex = pulseAmplitudeIndex,
timestamp = timestamp,
isDeviceClockSet = isDeviceClockSet
)
}
}
@@ -0,0 +1,59 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import org.hydev.wearsync.bles.ObservationUnit
import java.nio.ByteOrder
import java.util.*
enum class TemperatureType(val value: Int) {
Unknown(0), Armpit(1), Body(2), Ear(3), Finger(4), GastroIntestinalTract(5), Mouth(6), Rectum(7), Toe(8), Tympanum(9);
companion object {
fun fromValue(value: Int): TemperatureType
{
for (type in values()) {
if (type.value == value) return type
}
return Unknown
}
}
}
data class TemperatureMeasurement(
val temperatureValue: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val type: TemperatureType,
val createdAt: Date = Calendar.getInstance().time
)
/**
* Health Thermometer service
*/
class TemperatureDecoder : IDecoder<TemperatureMeasurement> {
override val sid = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): TemperatureMeasurement
{
val parser = BluetoothBytesParser(value, ByteOrder.LITTLE_ENDIAN)
val flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) ObservationUnit.Fahrenheit else ObservationUnit.Celsius
val timestampPresent = flags and 0x02 > 0
val typePresent = flags and 0x04 > 0
val temperatureValue = parser.getFloatValue(BluetoothBytesParser.FORMAT_FLOAT)
val timestamp = if (timestampPresent) parser.dateTime else null
val type = if (typePresent) TemperatureType.fromValue(parser.getIntValue(
BluetoothBytesParser.FORMAT_UINT8
)) else TemperatureType.Unknown
return TemperatureMeasurement(
unit = unit,
temperatureValue = temperatureValue,
timestamp = timestamp,
type = type
)
}
}
@@ -0,0 +1,52 @@
package org.hydev.wearsync.bles.decoders
import com.welie.blessed.BluetoothBytesParser
import org.hydev.wearsync.bles.ObservationUnit
import java.util.*
import kotlin.math.round
data class WeightMeasurement(
val weight: Float,
val unit: ObservationUnit,
val timestamp: Date?,
val userID: Int?,
val bmi: Float?,
val heightInMetersOrInches: Float?,
val createdAt: Date = Calendar.getInstance().time
)
/**
* Weight Scale Service
*/
class WeightDecoder : IDecoder<WeightMeasurement> {
override val sid = UUID.fromString("0000181D-0000-1000-8000-00805f9b34fb")
override val cid = UUID.fromString("00002A9D-0000-1000-8000-00805f9b34fb")
override fun decode(value: ByteArray): WeightMeasurement
{
val parser = BluetoothBytesParser(value)
val flags = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8)
val unit = if (flags and 0x01 > 0) ObservationUnit.Pounds else ObservationUnit.Kilograms
val timestampPresent = flags and 0x02 > 0
val userIDPresent = flags and 0x04 > 0
val bmiAndHeightPresent = flags and 0x08 > 0
val weightMultiplier = if (unit == ObservationUnit.Kilograms) 0.005f else 0.01f
val weight = parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * weightMultiplier
val timestamp = if (timestampPresent) parser.dateTime else null
val userID = if (userIDPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT8) else null
val bmi = if (bmiAndHeightPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * 0.1f else null
val heightMultiplier = if (unit == ObservationUnit.Kilograms) 0.001f else 0.1f
val height = if (bmiAndHeightPresent) parser.getIntValue(BluetoothBytesParser.FORMAT_UINT16) * heightMultiplier else null
return WeightMeasurement(
weight = round(weight * 100) / 100,
unit = unit,
timestamp = timestamp,
userID = userID,
bmi = bmi,
heightInMetersOrInches = height
)
}
}