diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt deleted file mode 100644 index a8a8ebb0..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSData.kt +++ /dev/null @@ -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, -) diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSManager.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSManager.kt deleted file mode 100644 index 93b3a8ca..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSManager.kt +++ /dev/null @@ -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() - - 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() - .onEach { data.tryEmit(data.value.copyWithNewResponse(it)) } - .launchIn(scope) - - setIndicationCallback(bpmCharacteristic).asValidResponseFlow() - .onEach { data.tryEmit(data.value.copyWithNewResponse(it)) } - .launchIn(scope) - - setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow() - .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 - } - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt new file mode 100644 index 00000000..668da28e --- /dev/null +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt @@ -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 +) \ No newline at end of file diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/DataMapper.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/DataMapper.kt deleted file mode 100644 index 5efd28b0..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/DataMapper.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSRepository.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSRepository.kt deleted file mode 100644 index aeaaf140..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSRepository.kt +++ /dev/null @@ -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> = 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) - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt index da2621de..33bc2f87 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt @@ -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 ) { diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSMapper.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSMapper.kt deleted file mode 100644 index c4cecf58..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSMapper.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt index 939d6322..5f21341b 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt @@ -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) diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt index e8789407..2911be68 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt @@ -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()) } diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt index 8b51b307..23c10d02 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt @@ -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 -) : BPSViewState() - -internal object NoDeviceState : BPSViewState() +internal data class BPSViewState( + val result: BPSServiceData = BPSServiceData(), + val deviceName: String? = null +) diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt index de180339..d51294ca 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt @@ -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(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) { 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 { - analytics.logEvent(ProfileConnectedEvent(Profile.BPS)) - } - }.launchIn(viewModelScope) + 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)) + } } } diff --git a/profile_bps/src/main/res/values/strings.xml b/profile_bps/src/main/res/values/strings.xml index e465ae40..524087c9 100644 --- a/profile_bps/src/main/res/values/strings.xml +++ b/profile_bps/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Data + No data available. If you are using nRF DK\'s press button 1 to see the result. + Systolic Diastolic Mean AP