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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.bps.R import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData import no.nordicsemi.android.bps.data.BPSServiceData
@Composable @Composable
internal fun BPSContentView(state: BPSData, onEvent: (BPSViewEvent) -> Unit) { internal fun BPSContentView(state: BPSServiceData, onEvent: (BPSViewEvent) -> Unit) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally 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.DeviceConnectingView
import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView
import no.nordicsemi.android.common.ui.scanner.view.Reason import no.nordicsemi.android.common.ui.scanner.view.Reason
import no.nordicsemi.android.service.ConnectedResult import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
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.ui.view.BackIconAppBar import no.nordicsemi.android.ui.view.BackIconAppBar
import no.nordicsemi.android.ui.view.LoggerIconAppBar import no.nordicsemi.android.ui.view.LoggerIconAppBar
import no.nordicsemi.android.ui.view.NavigateUpButton import no.nordicsemi.android.ui.view.NavigateUpButton
@@ -78,17 +70,15 @@ fun BPSScreen() {
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
when (state) { if (state.deviceName == null) {
NoDeviceState -> DeviceConnectingView() DeviceConnectingView()
is WorkingState -> when (state.result) { } else {
is IdleResult, when (state.result.connectionState) {
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } null,
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTED,
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTED -> BPSContentView(state.result) { viewModel.onEvent(it) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> BPSContentView(state.result.data) { viewModel.onEvent(it) }
} }
} }
} }
@@ -97,14 +87,10 @@ fun BPSScreen() {
@Composable @Composable
private fun AppBar(state: BPSViewState, navigateUp: () -> Unit, viewModel: BPSViewModel) { private fun AppBar(state: BPSViewState, navigateUp: () -> Unit, viewModel: BPSViewModel) {
val toolbarName = (state as? WorkingState)?.let { if (state.deviceName == null) {
(it.result as? DeviceHolder)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.bps_title), navigateUp) BackIconAppBar(stringResource(id = R.string.bps_title), navigateUp)
} else { } else {
LoggerIconAppBar(toolbarName, { LoggerIconAppBar(state.deviceName, {
viewModel.onEvent(DisconnectEvent) viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) { }, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent) 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.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.bps.R 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.BatteryLevelView
import no.nordicsemi.android.ui.view.KeyValueField import no.nordicsemi.android.ui.view.KeyValueField
import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.ScreenSection
import no.nordicsemi.android.ui.view.SectionTitle import no.nordicsemi.android.ui.view.SectionTitle
@Composable @Composable
internal fun BPSSensorsReadingView(state: BPSData) { internal fun BPSSensorsReadingView(state: BPSServiceData) {
ScreenSection { ScreenSection {
Column { Column {
SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.bps_records)) 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)) Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.bps_pulse), it) 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 @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
BPSSensorsReadingView(BPSData()) BPSSensorsReadingView(BPSServiceData())
} }

View File

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

View File

@@ -31,42 +31,63 @@
package no.nordicsemi.android.bps.viewmodel package no.nordicsemi.android.bps.viewmodel
import android.annotation.SuppressLint
import android.content.Context
import android.os.ParcelUuid import android.os.ParcelUuid
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.Profile import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent 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.BPSViewEvent
import no.nordicsemi.android.bps.view.BPSViewState import no.nordicsemi.android.bps.view.BPSViewState
import no.nordicsemi.android.bps.view.DisconnectEvent 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.OpenLoggerEvent
import no.nordicsemi.android.bps.view.WorkingState
import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.kotlin.ble.core.ServerDevice 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 no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import java.util.*
import javax.inject.Inject 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 @HiltViewModel
internal class BPSViewModel @Inject constructor( internal class BPSViewModel @Inject constructor(
private val repository: BPSRepository, @ApplicationContext
private val context: Context,
private val navigationManager: Navigator, private val navigationManager: Navigator,
private val analytics: AppAnalytics private val analytics: AppAnalytics
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState) private val _state = MutableStateFlow(BPSViewState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
private lateinit var client: BleGattClient
init { init {
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(BPS_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(BPS_SERVICE_UUID))
@@ -78,24 +99,87 @@ internal class BPSViewModel @Inject constructor(
private fun handleArgs(result: NavigationResult<ServerDevice>) { private fun handleArgs(result: NavigationResult<ServerDevice>) {
when (result) { when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp() is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> connectDevice(result.value) is NavigationResult.Success -> startGattClient(result.value)
} }
} }
fun onEvent(event: BPSViewEvent) { fun onEvent(event: BPSViewEvent) {
when (event) { when (event) {
DisconnectEvent -> navigationManager.navigateUp() DisconnectEvent -> navigationManager.navigateUp()
OpenLoggerEvent -> repository.openLogger() OpenLoggerEvent -> TODO()
} }
} }
private fun connectDevice(device: ServerDevice) { private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch {
repository.downloadData(viewModelScope, device).onEach { _state.value = _state.value.copy(deviceName = blinkyDevice.name)
_state.value = WorkingState(it)
(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)) analytics.logEvent(ProfileConnectedEvent(Profile.BPS))
} }
}.launchIn(viewModelScope)
} }
} }

View File

@@ -35,6 +35,8 @@
<string name="bps_records">Data</string> <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_systolic">Systolic</string>
<string name="bps_diastolic">Diastolic</string> <string name="bps_diastolic">Diastolic</string>
<string name="bps_mean">Mean AP</string> <string name="bps_mean">Mean AP</string>