[O] Refactor notification system
This commit is contained in:
@@ -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.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
|
import org.hydev.wearsync.bles.decoders.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import timber.log.Timber.DebugTree
|
import timber.log.Timber.DebugTree
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
@@ -13,6 +14,7 @@ import java.util.*
|
|||||||
internal class BluetoothHandler private constructor(context: Context) {
|
internal class BluetoothHandler private constructor(context: Context) {
|
||||||
|
|
||||||
private var currentTimeCounter = 0
|
private var currentTimeCounter = 0
|
||||||
|
val batteryChannel = Channel<UInt>(UNLIMITED)
|
||||||
val heartRateChannel = Channel<HeartRateMeasurement>(UNLIMITED)
|
val heartRateChannel = Channel<HeartRateMeasurement>(UNLIMITED)
|
||||||
val bloodPressureChannel = Channel<BloodPressureMeasurement>(UNLIMITED)
|
val bloodPressureChannel = Channel<BloodPressureMeasurement>(UNLIMITED)
|
||||||
val glucoseChannel = Channel<GlucoseMeasurement>(UNLIMITED)
|
val glucoseChannel = Channel<GlucoseMeasurement>(UNLIMITED)
|
||||||
@@ -24,24 +26,12 @@ internal class BluetoothHandler private constructor(context: Context) {
|
|||||||
private fun handlePeripheral(peripheral: BluetoothPeripheral) {
|
private fun handlePeripheral(peripheral: BluetoothPeripheral) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
val mtu = peripheral.requestMtu(185)
|
Timber.i("MTU is ${peripheral.requestMtu(185)}")
|
||||||
Timber.i("MTU is $mtu")
|
|
||||||
|
|
||||||
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
||||||
|
Timber.i("RSSI is ${peripheral.readRemoteRssi()}")
|
||||||
val rssi = peripheral.readRemoteRssi()
|
Timber.i("Received: ${peripheral.read(ManufacturerDecoder())}")
|
||||||
Timber.i("RSSI is $rssi")
|
Timber.i("Received: ${peripheral.read(ModelDecoder())}")
|
||||||
|
Timber.i("Battery level: ${peripheral.read(BatteryDecoder())}")
|
||||||
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")
|
|
||||||
|
|
||||||
// Write Current Time if possible
|
// Write Current Time if possible
|
||||||
peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let {
|
peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let {
|
||||||
@@ -56,12 +46,14 @@ internal class BluetoothHandler private constructor(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupHRSnotifications(peripheral)
|
peripheral.setupNotification(batteryChannel, BatteryDecoder())
|
||||||
setupPLXnotifications(peripheral)
|
peripheral.setupNotification(heartRateChannel, HeartRateDecoder())
|
||||||
setupHTSnotifications(peripheral)
|
peripheral.setupNotification(temperatureChannel, TemperatureDecoder())
|
||||||
|
peripheral.setupNotification(weightChannel, WeightDecoder())
|
||||||
|
peripheral.setupNotification(bloodPressureChannel, BloodPressureDecoder())
|
||||||
|
peripheral.setupNotification(pulseOxSpotChannel, PLXSpotDecoder())
|
||||||
|
peripheral.setupNotification(pulseOxContinuousChannel, PLXContinuousDecoder())
|
||||||
setupGLXnotifications(peripheral)
|
setupGLXnotifications(peripheral)
|
||||||
setupBLPnotifications(peripheral)
|
|
||||||
setupWSSnotifications(peripheral)
|
|
||||||
setupCTSnotifications(peripheral)
|
setupCTSnotifications(peripheral)
|
||||||
|
|
||||||
peripheral.getCharacteristic(CONTOUR_SERVICE_UUID, CONTOUR_CLOCK)?.let {
|
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) {
|
private suspend fun setupGLXnotifications(peripheral: BluetoothPeripheral) {
|
||||||
peripheral.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
|
peripheral.setupNotification(glucoseChannel, GlucoseDecoder())
|
||||||
peripheral.observe(it) { value ->
|
|
||||||
val measurement = GlucoseMeasurement.fromBytes(value)
|
|
||||||
glucoseChannel.trySend(measurement)
|
|
||||||
Timber.d("%s", measurement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peripheral.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID)?.let {
|
peripheral.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID)?.let {
|
||||||
val result = peripheral.observe(it) { value ->
|
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)
|
peripheral.writeCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID, command, WriteType.WITH_RESPONSE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setupBLPnotifications(peripheral: BluetoothPeripheral) {
|
suspend fun <T> BluetoothPeripheral.setupNotification(channel: Channel<T>, dec: IDecoder<T>)
|
||||||
peripheral.getCharacteristic(BLP_SERVICE_UUID, BLP_MEASUREMENT_CHARACTERISTIC_UUID)?.let {
|
{
|
||||||
peripheral.observe(it) { value ->
|
getCharacteristic(dec.sid, dec.cid)?.let {
|
||||||
val measurement = BloodPressureMeasurement.fromBytes(value)
|
observe(it) { value ->
|
||||||
bloodPressureChannel.trySend(measurement)
|
val measurement = dec.decode(value)
|
||||||
Timber.d("%s", measurement)
|
channel.trySend(measurement)
|
||||||
}
|
Timber.d(measurement.toString())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,44 +173,18 @@ internal class BluetoothHandler private constructor(context: Context) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// UUIDs for the Blood Pressure service (BLP)
|
// UUIDs for the Blood Pressure service (BLP)
|
||||||
val BLP_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
|
val BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
|
||||||
val BLP_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
|
val BLP_MEASUREMENT_CHARACTERISTIC_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")
|
|
||||||
|
|
||||||
// UUIDs for the Current Time service (CTS)
|
// UUIDs for the Current Time service (CTS)
|
||||||
val CTS_SERVICE_UUID: UUID = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb")
|
val CTS_SERVICE_UUID = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb")
|
||||||
val CURRENT_TIME_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A2B-0000-1000-8000-00805f9b34fb")
|
val CURRENT_TIME_CHARACTERISTIC_UUID = UUID.fromString("00002A2B-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
// UUIDs for the Battery Service (BAS)
|
val GLUCOSE_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
||||||
val BTS_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
|
val GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID = UUID.fromString("00002A52-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")
|
|
||||||
|
|
||||||
// Contour Glucose Service
|
// 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")
|
val CONTOUR_CLOCK = UUID.fromString("00001026-0002-11E2-9E96-0800200C9A66")
|
||||||
|
|
||||||
private var instance: BluetoothHandler? = null
|
private var instance: BluetoothHandler? = null
|
||||||
@@ -283,6 +194,9 @@ internal class BluetoothHandler private constructor(context: Context) {
|
|||||||
}
|
}
|
||||||
return instance!!
|
return instance!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> BluetoothPeripheral.read(decoder: IDecoder<T>) =
|
||||||
|
getCharacteristic(decoder.sid, decoder.cid)?.let { decoder.decode(readCharacteristic(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmField
|
@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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user