From 8266c2cda5fbce40172a4a1bf9e34fc283e438f9 Mon Sep 17 00:00:00 2001 From: Azalea Gui Date: Mon, 23 Jan 2023 14:27:42 -0500 Subject: [PATCH] [+] Add blessed example files --- .../wearsync/bles/BloodPressureMeasurement.kt | 52 +++ .../bles/BloodPressureMeasurementStatus.kt | 32 ++ .../hydev/wearsync/bles/BluetoothHandler.kt | 326 +++++++++++++++++ .../hydev/wearsync/bles/GlucoseMeasurement.kt | 49 +++ .../wearsync/bles/HeartRateMeasurement.kt | 51 +++ .../org/hydev/wearsync/bles/MainActivity.kt | 334 ++++++++++++++++++ .../hydev/wearsync/bles/ObservationUnit.kt | 17 + .../PulseOximeterContinuousMeasurement.kt | 56 +++ .../bles/PulseOximeterSpotMeasurement.kt | 49 +++ .../wearsync/bles/SensorContactFeature.kt | 9 + .../wearsync/bles/TemperatureMeasurement.kt | 37 ++ .../hydev/wearsync/bles/TemperatureType.kt | 15 + .../hydev/wearsync/bles/WeightMeasurement.kt | 48 +++ 13 files changed, 1075 insertions(+) create mode 100644 app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurementStatus.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/BluetoothHandler.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/GlucoseMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/HeartRateMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/MainActivity.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/ObservationUnit.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/PulseOximeterContinuousMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/PulseOximeterSpotMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/SensorContactFeature.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/TemperatureMeasurement.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/TemperatureType.kt create mode 100644 app/src/main/java/org/hydev/wearsync/bles/WeightMeasurement.kt diff --git a/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurement.kt new file mode 100644 index 0000000..5bb5a29 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurement.kt @@ -0,0 +1,52 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurementStatus.kt b/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurementStatus.kt new file mode 100644 index 0000000..a685b00 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/BloodPressureMeasurementStatus.kt @@ -0,0 +1,32 @@ +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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/BluetoothHandler.kt b/app/src/main/java/org/hydev/wearsync/bles/BluetoothHandler.kt new file mode 100644 index 0000000..858fc02 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/BluetoothHandler.kt @@ -0,0 +1,326 @@ +package org.hydev.wearsync.bles + +import android.bluetooth.BluetoothAdapter +import android.content.Context +import com.welie.blessed.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import timber.log.Timber +import timber.log.Timber.DebugTree +import java.nio.ByteOrder +import java.util.* + +internal class BluetoothHandler private constructor(context: Context) { + + private var currentTimeCounter = 0 + val heartRateChannel = Channel(UNLIMITED) + val bloodpressureChannel = Channel(UNLIMITED) + val glucoseChannel = Channel(UNLIMITED) + val pulseOxSpotChannel = Channel(UNLIMITED) + val pulseOxContinuousChannel = Channel(UNLIMITED) + val temperatureChannel = Channel(UNLIMITED) + val weightChannel = Channel(UNLIMITED) + + private fun handlePeripheral(peripheral: BluetoothPeripheral) { + scope.launch { + try { + val mtu = peripheral.requestMtu(185) + Timber.i("MTU is $mtu") + + 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") + + // Write Current Time if possible + peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let { + // If it has the write property we write the current time + if (it.supportsWritingWithResponse()) { + // Write the current time unless it is an Omron device + if (!peripheral.name.contains("BLEsmart_", true)) { + val parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) + parser.setCurrentTime(Calendar.getInstance()) + peripheral.writeCharacteristic(it, parser.value, WriteType.WITH_RESPONSE) + } + } + } + + setupHRSnotifications(peripheral) + setupPLXnotifications(peripheral) + setupHTSnotifications(peripheral) + setupGLXnotifications(peripheral) + setupBLPnotifications(peripheral) + setupWSSnotifications(peripheral) + setupCTSnotifications(peripheral) + + peripheral.getCharacteristic(CONTOUR_SERVICE_UUID, CONTOUR_CLOCK)?.let { + writeContourClock(peripheral) + } + } catch (e: IllegalArgumentException) { + Timber.e(e) + } catch (b: GattException) { + Timber.e(b) + } + } + } + + private suspend fun writeContourClock(peripheral: BluetoothPeripheral) { + val calendar = Calendar.getInstance() + val offsetInMinutes = calendar.timeZone.rawOffset / 60000 + calendar.timeZone = TimeZone.getTimeZone("UTC") + val parser = BluetoothBytesParser(ByteOrder.LITTLE_ENDIAN) + parser.setIntValue(1, BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(calendar[Calendar.YEAR], BluetoothBytesParser.FORMAT_UINT16) + parser.setIntValue(calendar[Calendar.MONTH] + 1, BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(calendar[Calendar.DAY_OF_MONTH], BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(calendar[Calendar.HOUR_OF_DAY], BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(calendar[Calendar.MINUTE], BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(calendar[Calendar.SECOND], BluetoothBytesParser.FORMAT_UINT8) + parser.setIntValue(offsetInMinutes, BluetoothBytesParser.FORMAT_SINT16) + peripheral.writeCharacteristic(CONTOUR_SERVICE_UUID, CONTOUR_CLOCK, parser.value, WriteType.WITH_RESPONSE) + } + + private suspend fun setupCTSnotifications(peripheral: BluetoothPeripheral) { + peripheral.getCharacteristic(CTS_SERVICE_UUID, CURRENT_TIME_CHARACTERISTIC_UUID)?.let { currentTimeCharacteristic -> + peripheral.observe(currentTimeCharacteristic) { value -> + val parser = BluetoothBytesParser(value) + val currentTime = parser.dateTime + Timber.i("Received device time: %s", currentTime) + + // Deal with Omron devices where we can only write currentTime under specific conditions + val name = peripheral.name + if (name.contains("BLEsmart_", true)) { + peripheral.getCharacteristic(BLP_SERVICE_UUID, BLP_MEASUREMENT_CHARACTERISTIC_UUID)?.let { + val isNotifying = peripheral.isNotifying(it) + if (isNotifying) currentTimeCounter++ + + // We can set device time for Omron devices only if it is the first notification and currentTime is more than 10 min from now + val interval = Math.abs(Calendar.getInstance().timeInMillis - currentTime.time) + if (currentTimeCounter == 1 && interval > 10 * 60 * 1000) { + parser.setCurrentTime(Calendar.getInstance()) + scope.launch { + peripheral.writeCharacteristic(it, parser.value, WriteType.WITH_RESPONSE) + } + } + } + } + } + } + } + + 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.getCharacteristic(GLUCOSE_SERVICE_UUID, GLUCOSE_RECORD_ACCESS_POINT_CHARACTERISTIC_UUID)?.let { + val result = peripheral.observe(it) { value -> + Timber.d("record access response: ${value.asHexString()}") + } + + if (result) { + writeGetAllGlucoseMeasurements(peripheral) + } + } + } + + private suspend fun writeGetAllGlucoseMeasurements(peripheral: BluetoothPeripheral) { + val OP_CODE_REPORT_STORED_RECORDS: Byte = 1 + val OPERATOR_ALL_RECORDS: Byte = 1 + val command = byteArrayOf(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_ALL_RECORDS) + 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) + } + } + } + + private fun startScanning() { + central.scanForPeripheralsWithServices( + supportedServices, + { peripheral, scanResult -> + Timber.i("Found peripheral '${peripheral.name}' with RSSI ${scanResult.rssi}") + central.stopScan() + connectPeripheral(peripheral) + }, + { scanFailure -> Timber.e("scan failed with reason $scanFailure") }) + } + + private fun connectPeripheral(peripheral: BluetoothPeripheral) { + peripheral.observeBondState { + Timber.i("Bond state is $it") + } + + scope.launch { + try { + central.connectPeripheral(peripheral) + } catch (connectionFailed: ConnectionFailedException) { + Timber.e("connection failed") + } + } + } + + companion object { + // UUIDs for the Blood Pressure service (BLP) + private val BLP_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") + private val BLP_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") + + // UUIDs for the Health Thermometer service (HTS) + private val HTS_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") + private val HTS_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") + + // UUIDs for the Heart Rate service (HRS) + private val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") + private val HRS_MEASUREMENT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb") + + // UUIDs for the Device Information service (DIS) + private val DIS_SERVICE_UUID: UUID = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb") + private val MANUFACTURER_NAME_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A29-0000-1000-8000-00805f9b34fb") + private val MODEL_NUMBER_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A24-0000-1000-8000-00805f9b34fb") + + // UUIDs for the Current Time service (CTS) + private val CTS_SERVICE_UUID: UUID = UUID.fromString("00001805-0000-1000-8000-00805f9b34fb") + private val CURRENT_TIME_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A2B-0000-1000-8000-00805f9b34fb") + + // UUIDs for the Battery Service (BAS) + private val BTS_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") + private 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") + private val PLX_SPOT_MEASUREMENT_CHAR_UUID: UUID = UUID.fromString("00002a5e-0000-1000-8000-00805f9b34fb") + private 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") + private 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_MEASUREMENT_CONTEXT_CHARACTERISTIC_UUID: UUID = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") + + // Contour Glucose Service + val CONTOUR_SERVICE_UUID: UUID = UUID.fromString("00000000-0002-11E2-9E96-0800200C9A66") + private val CONTOUR_CLOCK = UUID.fromString("00001026-0002-11E2-9E96-0800200C9A66") + private var instance: BluetoothHandler? = null + + private val supportedServices = arrayOf(BLP_SERVICE_UUID, HTS_SERVICE_UUID, HRS_SERVICE_UUID, PLX_SERVICE_UUID, WSS_SERVICE_UUID, GLUCOSE_SERVICE_UUID) + + @JvmStatic + @Synchronized + fun getInstance(context: Context): BluetoothHandler + { + if (instance == null) { + instance = BluetoothHandler(context.applicationContext) + } + return requireNotNull(instance) + } + } + + @JvmField + var central: BluetoothCentralManager + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + init { + Timber.plant(DebugTree()) + central = BluetoothCentralManager(context) + + central.observeConnectionState { peripheral, state -> + Timber.i("Peripheral '${peripheral.name}' is $state") + when (state) { + ConnectionState.CONNECTED -> handlePeripheral(peripheral) + ConnectionState.DISCONNECTED -> scope.launch { + delay(15000) + + // Check if this peripheral should still be auto connected + if (central.getPeripheral(peripheral.address).getState() == ConnectionState.DISCONNECTED) { + central.autoConnectPeripheral(peripheral) + } + } + else -> { + } + } + } + + central.observeAdapterState { state -> + when (state) { + BluetoothAdapter.STATE_ON -> startScanning() + } + } + + startScanning() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/GlucoseMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/GlucoseMeasurement.kt new file mode 100644 index 0000000..7453022 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/GlucoseMeasurement.kt @@ -0,0 +1,49 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/HeartRateMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/HeartRateMeasurement.kt new file mode 100644 index 0000000..969672b --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/HeartRateMeasurement.kt @@ -0,0 +1,51 @@ +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() + 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() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/MainActivity.kt b/app/src/main/java/org/hydev/wearsync/bles/MainActivity.kt new file mode 100644 index 0000000..d1817d0 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/MainActivity.kt @@ -0,0 +1,334 @@ +package org.hydev.wearsync.bles + +import android.Manifest +import android.app.AlertDialog +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.content.* +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.view.View +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import com.welie.blessed.BluetoothPeripheral +import org.hydev.wearsync.bles.BluetoothHandler.Companion.getInstance +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.consumeAsFlow +import org.hydev.wearsync.R +import timber.log.Timber +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +class MainActivity : AppCompatActivity() { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var measurementValue: TextView? = null + private val dateFormat: DateFormat = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH) + private val enableBluetoothRequest = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_OK) { + // Bluetooth has been enabled + checkPermissions() + } else { + // Bluetooth has not been enabled, try again + askToEnableBluetooth() + } + } + + private val bluetoothManager by lazy { + applicationContext + .getSystemService(BLUETOOTH_SERVICE) + as BluetoothManager + } + + + private fun askToEnableBluetooth() { + val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + enableBluetoothRequest.launch(enableBtIntent) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + measurementValue = findViewById(R.id.bloodPressureValue) as TextView + registerReceiver( + locationServiceStateReceiver, + IntentFilter(LocationManager.MODE_CHANGED_ACTION) + ) + } + + override fun onResume() { + super.onResume() + if (bluetoothManager.adapter != null) { + if (!isBluetoothEnabled) { + askToEnableBluetooth() + } else { + checkPermissions() + } + } else { + Timber.e("This device has no Bluetooth hardware") + } + } + + private val isBluetoothEnabled: Boolean + get() { + val bluetoothAdapter = bluetoothManager.adapter ?: return false + return bluetoothAdapter.isEnabled + } + + + private fun initBluetoothHandler() { + val bluetoothHandler = getInstance(applicationContext) + + collectBloodPressure(bluetoothHandler) + collectHeartRate(bluetoothHandler) + collectGlucose(bluetoothHandler) + collectPulseOxContinuous(bluetoothHandler) + collectPulseOxSpot(bluetoothHandler) + collectTemperature(bluetoothHandler) + collectWeight(bluetoothHandler) + } + + private fun collectBloodPressure(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.bloodpressureChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue!!.text = String.format( + Locale.ENGLISH, + "%.0f/%.0f %s, %.0f bpm\n%s\n", + it.systolic, + it.diastolic, + if (it.unit == ObservationUnit.MMHG) "mmHg" else "kpa", + it.pulseRate, + dateFormat.format(it.timestamp ?: Calendar.getInstance()) + ) + } + } + } + } + + private fun collectGlucose(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.glucoseChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue!!.text = String.format( + Locale.ENGLISH, + "%.1f %s\n%s\n", + it.value, + if (it.unit === ObservationUnit.MmolPerLiter) "mmol/L" else "mg/dL", + dateFormat.format(it.timestamp ?: Calendar.getInstance()), + ) + } + } + } + } + + private fun collectHeartRate(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.heartRateChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue?.text = String.format(Locale.ENGLISH, "%d bpm", it.pulse) + } + } + } + } + + private fun collectPulseOxContinuous(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.pulseOxContinuousChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue!!.text = String.format( + Locale.ENGLISH, + "SpO2 %d%%, Pulse %d bpm\n%s\n\nfrom %s", + it.spO2, + it.pulseRate, + dateFormat.format(Calendar.getInstance()) + ) + } + } + } + } + + private fun collectPulseOxSpot(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.pulseOxSpotChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue!!.text = String.format( + Locale.ENGLISH, + "SpO2 %d%%, Pulse %d bpm\n", + it.spO2, + it.pulseRate + ) + } + } + } + } + + private fun collectTemperature(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.temperatureChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue?.text = String.format( + Locale.ENGLISH, + "%.1f %s (%s)\n%s\n", + it.temperatureValue, + if (it.unit == ObservationUnit.Celsius) "celsius" else "fahrenheit", + it.type, + dateFormat.format(it.timestamp ?: Calendar.getInstance()) + ) + } + } + } + } + + private fun collectWeight(bluetoothHandler: BluetoothHandler) { + scope.launch { + bluetoothHandler.weightChannel.consumeAsFlow().collect { + withContext(Dispatchers.Main) { + measurementValue!!.text = String.format( + Locale.ENGLISH, + "%.1f %s\n%s\n", + it.weight, it.unit.toString(), + dateFormat.format(it.timestamp ?: Calendar.getInstance()) + ) + } + } + } + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(locationServiceStateReceiver) + } + + private val locationServiceStateReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (action != null && action == LocationManager.MODE_CHANGED_ACTION) { + val isEnabled = areLocationServicesEnabled() + Timber.i("Location service state changed to: %s", if (isEnabled) "on" else "off") + checkPermissions() + } + } + } + + private fun getPeripheral(peripheralAddress: String): BluetoothPeripheral { + val central = getInstance(applicationContext).central + return central.getPeripheral(peripheralAddress) + } + + private fun checkPermissions() { + val missingPermissions = getMissingPermissions(requiredPermissions) + if (missingPermissions.isNotEmpty()) { + requestPermissions(missingPermissions, ACCESS_LOCATION_REQUEST) + } else { + checkIfLocationIsNeeded() + } + } + + private fun getMissingPermissions(requiredPermissions: Array): Array { + val missingPermissions: MutableList = ArrayList() + for (requiredPermission in requiredPermissions) { + if (applicationContext.checkSelfPermission(requiredPermission) != PackageManager.PERMISSION_GRANTED) { + missingPermissions.add(requiredPermission) + } + } + return missingPermissions.toTypedArray() + } + + private val requiredPermissions: Array + get() { + val targetSdkVersion = applicationInfo.targetSdkVersion + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && targetSdkVersion >= Build.VERSION_CODES.S) { + arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && targetSdkVersion >= Build.VERSION_CODES.Q) { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) + } else arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + } + + private fun checkIfLocationIsNeeded() { + val targetSdkVersion = applicationInfo.targetSdkVersion + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && targetSdkVersion < Build.VERSION_CODES.S) { + // Check if Location services are on because they are required to make scanning work for SDK < 31 + if (checkLocationServices()) { + initBluetoothHandler() + } + } else { + initBluetoothHandler() + } + } + + private fun areLocationServicesEnabled(): Boolean { + val locationManager = + applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + locationManager.isLocationEnabled + } else { + val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + val isNetworkEnabled = + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + isGpsEnabled || isNetworkEnabled + } + } + + private fun checkLocationServices(): Boolean { + return if (!areLocationServicesEnabled()) { + AlertDialog.Builder(this@MainActivity) + .setTitle("Location services are not enabled") + .setMessage("Scanning for Bluetooth peripherals requires locations services to be enabled.") // Want to enable? + .setPositiveButton("Enable") { dialogInterface, _ -> + dialogInterface.cancel() + startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + } + .setNegativeButton("Cancel") { dialog, _ -> + // if this button is clicked, just close + // the dialog box and do nothing + dialog.cancel() + } + .create() + .show() + false + } else { + true + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + // Check if all permission were granted + var allGranted = true + for (result in grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + allGranted = false + break + } + } + if (allGranted) { + checkIfLocationIsNeeded() + } else { + AlertDialog.Builder(this@MainActivity) + .setTitle("Location permission is required for scanning Bluetooth peripherals") + .setMessage("Please grant permissions") + .setPositiveButton("Retry") { dialogInterface, _ -> + dialogInterface.cancel() + checkPermissions() + } + .create() + .show() + } + } + + companion object { + private const val REQUEST_ENABLE_BT = 1 + private const val ACCESS_LOCATION_REQUEST = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/ObservationUnit.kt b/app/src/main/java/org/hydev/wearsync/bles/ObservationUnit.kt new file mode 100644 index 0000000..1fddf38 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/ObservationUnit.kt @@ -0,0 +1,17 @@ +package org.hydev.wearsync.bles + +enum class ObservationUnit(val notation: String, val mdc: String) { + BeatsPerMinute("bpm", "MDC_DIM_BEAT_PER_MIN"), + Celsius("\u00B0C", "MDC_DIM_DEGC"), + Fahrenheit("\u00B0F", "MDC_DIM_FAHR"), + Inches("inch", "MDC_DIM_INCH"), + Kilograms("Kg", "MDC_DIM_KILO_G"), + KgM2("kg/m2", "MDC_DIM_KG_PER_M_SQ"), + KPA("kPa", "MDC_DIM_KILO_PASCAL"), + Meters("m", "MDC_DIM_M"), + MiligramPerDeciliter("mg/dL", "MDC_DIM_MILLI_G_PER_DL"), + MmolPerLiter("mmol/L", "MDC_DIM_MILLI_MOLE_PER_L"), + MMHG("mmHg", "MDC_DIM_MMHG"), + Percent("%", "MDC_DIM_PERCENT"), + Pounds("lbs", "MDC_DIM_LB"), +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterContinuousMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterContinuousMeasurement.kt new file mode 100644 index 0000000..c684722 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterContinuousMeasurement.kt @@ -0,0 +1,56 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterSpotMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterSpotMeasurement.kt new file mode 100644 index 0000000..127d441 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/PulseOximeterSpotMeasurement.kt @@ -0,0 +1,49 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/SensorContactFeature.kt b/app/src/main/java/org/hydev/wearsync/bles/SensorContactFeature.kt new file mode 100644 index 0000000..a16eeeb --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/SensorContactFeature.kt @@ -0,0 +1,9 @@ +package org.hydev.wearsync.bles + +/** + * Enum that contains all sensor contact feature as specified here: + * https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml + */ +enum class SensorContactFeature { + NotSupported, SupportedNoContact, SupportedAndContact +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/TemperatureMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/TemperatureMeasurement.kt new file mode 100644 index 0000000..76c7c85 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/TemperatureMeasurement.kt @@ -0,0 +1,37 @@ +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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/TemperatureType.kt b/app/src/main/java/org/hydev/wearsync/bles/TemperatureType.kt new file mode 100644 index 0000000..7db5307 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/TemperatureType.kt @@ -0,0 +1,15 @@ +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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hydev/wearsync/bles/WeightMeasurement.kt b/app/src/main/java/org/hydev/wearsync/bles/WeightMeasurement.kt new file mode 100644 index 0000000..4a9c787 --- /dev/null +++ b/app/src/main/java/org/hydev/wearsync/bles/WeightMeasurement.kt @@ -0,0 +1,48 @@ +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 + ) + } + } +} \ No newline at end of file