Migrate BPS profile to the new BLE library

This commit is contained in:
Sylwester Zielinski
2023-03-10 15:48:01 +01:00
parent 205b94cfa3
commit e448d2c1a4
12 changed files with 183 additions and 460 deletions

View File

@@ -1,48 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.bps.data
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
import java.util.*
data class BPSData(
val batteryLevel: Int? = null,
val cuffPressure: Float = 0f,
val unit: Int = 0,
val pulseRate: Float? = null,
val userID: Int? = null,
val status: BloodPressureTypes.BPMStatus? = null,
val calendar: Calendar? = null,
val systolic: Float = 0f,
val diastolic: Float = 0f,
val meanArterialPressure: Float = 0f,
)

View File

@@ -1,133 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.bps.data
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse
import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.service.ConnectionObserverAdapter
import java.util.*
val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class BPSManager(
@ApplicationContext context: Context,
private val scope: CoroutineScope,
private val logger: NordicLogger
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var bpmCharacteristic: BluetoothGattCharacteristic? = null
private var icpCharacteristic: BluetoothGattCharacteristic? = null
private val data = MutableStateFlow(BPSData())
val dataHolder = ConnectionObserverAdapter<BPSData>()
init {
connectionObserver = dataHolder
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun log(priority: Int, message: String) {
logger.log(priority, message)
}
override fun getMinLogPriority(): Int {
return Log.VERBOSE
}
override fun getGattCallback(): BleManagerGattCallback {
return BloodPressureManagerGattCallback()
}
private inner class BloodPressureManagerGattCallback : BleManagerGattCallback() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun initialize() {
super.initialize()
setNotificationCallback(icpCharacteristic).asValidResponseFlow<IntermediateCuffPressureResponse>()
.onEach { data.tryEmit(data.value.copyWithNewResponse(it)) }
.launchIn(scope)
setIndicationCallback(bpmCharacteristic).asValidResponseFlow<BloodPressureMeasurementResponse>()
.onEach { data.tryEmit(data.value.copyWithNewResponse(it)) }
.launchIn(scope)
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(icpCharacteristic).enqueue()
enableIndications(bpmCharacteristic).enqueue()
enableNotifications(batteryLevelCharacteristic).enqueue()
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
gatt.getService(BPS_SERVICE_UUID)?.run {
bpmCharacteristic = getCharacteristic(BPM_CHARACTERISTIC_UUID)
icpCharacteristic = getCharacteristic(ICP_CHARACTERISTIC_UUID)
}
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return bpmCharacteristic != null
}
override fun onServicesInvalidated() {
icpCharacteristic = null
bpmCharacteristic = null
batteryLevelCharacteristic = null
}
}
}

View File

@@ -0,0 +1,12 @@
package no.nordicsemi.android.bps.data
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.bps.BloodPressureMeasurementData
import no.nordicsemi.android.kotlin.ble.profile.bps.IntermediateCuffPressureData
data class BPSServiceData (
val bloodPressureMeasurement: BloodPressureMeasurementData? = null,
val intermediateCuffPressure: IntermediateCuffPressureData? = null,
val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null
)

View File

@@ -1,62 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.bps.data
import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse
import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse
internal fun BPSData.copyWithNewResponse(response: IntermediateCuffPressureResponse): BPSData {
return with (response) {
copy(
cuffPressure = cuffPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
calendar = timestamp
)
}
}
internal fun BPSData.copyWithNewResponse(response: BloodPressureMeasurementResponse): BPSData {
return with (response) {
copy(
systolic = systolic,
diastolic = diastolic,
meanArterialPressure = meanArterialPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
)
}
}

View File

@@ -1,97 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.bps.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.data.BPSManager
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.common.logger.NordicLoggerFactory
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.ui.view.StringConst
import javax.inject.Inject
@ViewModelScoped
internal class BPSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val loggerFactory: NordicLoggerFactory,
private val stringConst: StringConst
) {
private var logger: NordicLogger? = null
fun downloadData(scope: CoroutineScope, device: ServerDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "BPS", device.address).also {
logger = it
}
val manager = BPSManager(context, scope, createdLogger)
manager.dataHolder.status.onEach {
trySend(it)
}.launchIn(scope)
scope.launch {
manager.start(device)
}
awaitClose {
manager.disconnect().enqueue()
logger = null
}
}
private suspend fun BPSManager.start(device: ServerDevice) {
// try {
// connect(device.device)
// .useAutoConnect(false)
// .retry(3, 100)
// .suspend()
// } catch (e: Exception) {
// e.printStackTrace()
// }
}
fun openLogger() {
NordicLogger.launch(context, logger)
}
}

View File

@@ -42,10 +42,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.data.BPSServiceData
@Composable
internal fun BPSContentView(state: BPSData, onEvent: (BPSViewEvent) -> Unit) {
internal fun BPSContentView(state: BPSServiceData, onEvent: (BPSViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {

View File

@@ -1,57 +0,0 @@
/*
* Copyright (c) 2022, Nordic Semiconductor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
* of conditions and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package no.nordicsemi.android.bps.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
@Composable
fun BPSData.displaySystolic(): String {
return stringResource(id = R.string.bps_blood_pressure, systolic)
}
@Composable
fun BPSData.displayDiastolic(): String {
return stringResource(id = R.string.bps_blood_pressure, diastolic)
}
@Composable
fun BPSData.displayMeanArterialPressure(): String {
return stringResource(id = R.string.bps_blood_pressure, meanArterialPressure)
}
@Composable
fun BPSData.displayHeartRate(): String? {
return pulseRate?.toString()
}

View File

@@ -48,15 +48,7 @@ import no.nordicsemi.android.bps.viewmodel.BPSViewModel
import no.nordicsemi.android.common.ui.scanner.view.DeviceConnectingView
import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView
import no.nordicsemi.android.common.ui.scanner.view.Reason
import no.nordicsemi.android.service.ConnectedResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.DeviceHolder
import no.nordicsemi.android.service.DisconnectedResult
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.service.LinkLossResult
import no.nordicsemi.android.service.MissingServiceResult
import no.nordicsemi.android.service.SuccessResult
import no.nordicsemi.android.service.UnknownErrorResult
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.ui.view.BackIconAppBar
import no.nordicsemi.android.ui.view.LoggerIconAppBar
import no.nordicsemi.android.ui.view.NavigateUpButton
@@ -78,17 +70,15 @@ fun BPSScreen() {
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
when (state) {
NoDeviceState -> DeviceConnectingView()
is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) }
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> BPSContentView(state.result.data) { viewModel.onEvent(it) }
if (state.deviceName == null) {
DeviceConnectingView()
} else {
when (state.result.connectionState) {
null,
GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
GattConnectionState.STATE_DISCONNECTED,
GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
GattConnectionState.STATE_CONNECTED -> BPSContentView(state.result) { viewModel.onEvent(it) }
}
}
}
@@ -97,14 +87,10 @@ fun BPSScreen() {
@Composable
private fun AppBar(state: BPSViewState, navigateUp: () -> Unit, viewModel: BPSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? DeviceHolder)?.deviceName()
}
if (toolbarName == null) {
if (state.deviceName == null) {
BackIconAppBar(stringResource(id = R.string.bps_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, {
LoggerIconAppBar(state.deviceName, {
viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)

View File

@@ -34,34 +34,45 @@ package no.nordicsemi.android.bps.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.data.BPSServiceData
import no.nordicsemi.android.kotlin.ble.profile.bps.BloodPressureMeasurementData
import no.nordicsemi.android.kotlin.ble.profile.bps.IntermediateCuffPressureData
import no.nordicsemi.android.ui.view.BatteryLevelView
import no.nordicsemi.android.ui.view.KeyValueField
import no.nordicsemi.android.ui.view.ScreenSection
import no.nordicsemi.android.ui.view.SectionTitle
@Composable
internal fun BPSSensorsReadingView(state: BPSData) {
internal fun BPSSensorsReadingView(state: BPSServiceData) {
ScreenSection {
Column {
SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.bps_records))
Spacer(modifier = Modifier.height(16.dp))
KeyValueField(stringResource(id = R.string.bps_systolic), state.displaySystolic())
Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_diastolic), state.displayDiastolic())
Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_mean), state.displayMeanArterialPressure())
state.displayHeartRate()?.let {
state.bloodPressureMeasurement?.let {
Spacer(modifier = Modifier.height(16.dp))
BloodPressureView(it)
}
state.intermediateCuffPressure?.displayHeartRate()?.let {
Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_pulse), it)
}
if (state.intermediateCuffPressure == null && state.bloodPressureMeasurement == null) {
Spacer(modifier = Modifier.height(16.dp))
Text(
stringResource(id = R.string.no_data_info),
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@@ -72,8 +83,37 @@ internal fun BPSSensorsReadingView(state: BPSData) {
}
}
@Composable
private fun BloodPressureView(state: BloodPressureMeasurementData) {
KeyValueField(stringResource(id = R.string.bps_systolic), state.displaySystolic())
Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_diastolic), state.displayDiastolic())
Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_mean), state.displayMeanArterialPressure())
}
@Composable
fun BloodPressureMeasurementData.displaySystolic(): String {
return stringResource(id = R.string.bps_blood_pressure, systolic)
}
@Composable
fun BloodPressureMeasurementData.displayDiastolic(): String {
return stringResource(id = R.string.bps_blood_pressure, diastolic)
}
@Composable
fun BloodPressureMeasurementData.displayMeanArterialPressure(): String {
return stringResource(id = R.string.bps_blood_pressure, meanArterialPressure)
}
@Composable
fun IntermediateCuffPressureData.displayHeartRate(): String? {
return pulseRate?.toString()
}
@Preview
@Composable
private fun Preview() {
BPSSensorsReadingView(BPSData())
BPSSensorsReadingView(BPSServiceData())
}

View File

@@ -31,13 +31,9 @@
package no.nordicsemi.android.bps.view
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.bps.data.BPSServiceData
internal sealed class BPSViewState
internal data class WorkingState(
val result: BleManagerResult<BPSData>
) : BPSViewState()
internal object NoDeviceState : BPSViewState()
internal data class BPSViewState(
val result: BPSServiceData = BPSServiceData(),
val deviceName: String? = null
)

View File

@@ -31,42 +31,63 @@
package no.nordicsemi.android.bps.viewmodel
import android.annotation.SuppressLint
import android.content.Context
import android.os.ParcelUuid
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent
import no.nordicsemi.android.bps.data.BPS_SERVICE_UUID
import no.nordicsemi.android.bps.repository.BPSRepository
import no.nordicsemi.android.bps.view.BPSViewEvent
import no.nordicsemi.android.bps.view.BPSViewState
import no.nordicsemi.android.bps.view.DisconnectEvent
import no.nordicsemi.android.bps.view.NoDeviceState
import no.nordicsemi.android.bps.view.OpenLoggerEvent
import no.nordicsemi.android.bps.view.WorkingState
import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.ConnectedResult
import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser
import no.nordicsemi.android.kotlin.ble.profile.bps.BloodPressureMeasurementData
import no.nordicsemi.android.kotlin.ble.profile.bps.BloodPressureMeasurementParser
import no.nordicsemi.android.kotlin.ble.profile.bps.IntermediateCuffPressureData
import no.nordicsemi.android.kotlin.ble.profile.bps.IntermediateCuffPressureParser
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import java.util.*
import javax.inject.Inject
val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
@SuppressLint("MissingPermission", "StaticFieldLeak")
@HiltViewModel
internal class BPSViewModel @Inject constructor(
private val repository: BPSRepository,
@ApplicationContext
private val context: Context,
private val navigationManager: Navigator,
private val analytics: AppAnalytics
) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
private val _state = MutableStateFlow(BPSViewState())
val state = _state.asStateFlow()
private lateinit var client: BleGattClient
init {
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(BPS_SERVICE_UUID))
@@ -78,24 +99,87 @@ internal class BPSViewModel @Inject constructor(
private fun handleArgs(result: NavigationResult<ServerDevice>) {
when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> connectDevice(result.value)
is NavigationResult.Success -> startGattClient(result.value)
}
}
fun onEvent(event: BPSViewEvent) {
when (event) {
DisconnectEvent -> navigationManager.navigateUp()
OpenLoggerEvent -> repository.openLogger()
OpenLoggerEvent -> TODO()
}
}
private fun connectDevice(device: ServerDevice) {
repository.downloadData(viewModelScope, device).onEach {
_state.value = WorkingState(it)
private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch {
_state.value = _state.value.copy(deviceName = blinkyDevice.name)
(it as? ConnectedResult)?.let {
client = blinkyDevice.connect(context)
client.connectionState
.filterNotNull()
.onEach { onDataUpdate(it) }
.onEach { stopIfDisconnected(it) }
.onEach { logAnalytics(it) }
.launchIn(viewModelScope)
client.services
.filterNotNull()
.onEach { configureGatt(it) }
.launchIn(viewModelScope)
}
private suspend fun configureGatt(services: BleGattServices) {
val bpsService = services.findService(BPS_SERVICE_UUID)!!
val bpmCharacteristic = bpsService.findCharacteristic(BPM_CHARACTERISTIC_UUID)!!
val icpCharacteristic = bpsService.findCharacteristic(ICP_CHARACTERISTIC_UUID)
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
batteryLevelCharacteristic.getNotifications()
.mapNotNull { BatteryLevelParser.parse(it) }
.onEach { onDataUpdate(it) }
.launchIn(viewModelScope)
bpmCharacteristic.getNotifications()
.mapNotNull { BloodPressureMeasurementParser.parse(it) }
.onEach { onDataUpdate(it) }
.launchIn(viewModelScope)
icpCharacteristic?.getNotifications()
?.mapNotNull { IntermediateCuffPressureParser.parse(it) }
?.onEach { onDataUpdate(it) }
?.launchIn(viewModelScope)
}
private fun onDataUpdate(connectionState: GattConnectionState) {
val newResult = _state.value.result.copy(connectionState = connectionState)
_state.value = _state.value.copy(result = newResult)
}
private fun onDataUpdate(batteryLevel: Int) {
val newResult = _state.value.result.copy(batteryLevel = batteryLevel)
_state.value = _state.value.copy(result = newResult)
}
private fun onDataUpdate(data: BloodPressureMeasurementData) {
val newResult = _state.value.result.copy(bloodPressureMeasurement = data)
_state.value = _state.value.copy(result = newResult)
}
private fun onDataUpdate(data: IntermediateCuffPressureData) {
val newResult = _state.value.result.copy(intermediateCuffPressure = data)
_state.value = _state.value.copy(result = newResult)
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
navigationManager.navigateUp()
}
}
private fun logAnalytics(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.BPS))
}
}.launchIn(viewModelScope)
}
}

View File

@@ -35,6 +35,8 @@
<string name="bps_records">Data</string>
<string name="no_data_info">No data available. If you are using nRF DK\'s press button 1 to see the result.</string>
<string name="bps_systolic">Systolic</string>
<string name="bps_diastolic">Diastolic</string>
<string name="bps_mean">Mean AP</string>