[+] Add blessed example files
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<HeartRateMeasurement>(UNLIMITED)
|
||||
val bloodpressureChannel = Channel<BloodPressureMeasurement>(UNLIMITED)
|
||||
val glucoseChannel = Channel<GlucoseMeasurement>(UNLIMITED)
|
||||
val pulseOxSpotChannel = Channel<PulseOximeterSpotMeasurement>(UNLIMITED)
|
||||
val pulseOxContinuousChannel = Channel<PulseOximeterContinuousMeasurement>(UNLIMITED)
|
||||
val temperatureChannel = Channel<TemperatureMeasurement>(UNLIMITED)
|
||||
val weightChannel = Channel<WeightMeasurement>(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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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,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<View>(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<String>): Array<String> {
|
||||
val missingPermissions: MutableList<String> = ArrayList()
|
||||
for (requiredPermission in requiredPermissions) {
|
||||
if (applicationContext.checkSelfPermission(requiredPermission) != PackageManager.PERMISSION_GRANTED) {
|
||||
missingPermissions.add(requiredPermission)
|
||||
}
|
||||
}
|
||||
return missingPermissions.toTypedArray()
|
||||
}
|
||||
|
||||
private val requiredPermissions: Array<String>
|
||||
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<String>,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user