Migrate HRS profile

This commit is contained in:
Sylwester Zielinski
2023-03-09 16:48:12 +01:00
parent 9d74000a03
commit 84eb61d59f
19 changed files with 160 additions and 272 deletions

View File

@@ -3,7 +3,7 @@ package no.nordicsemi.android.csc.data
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData
data class CSCServicesData( data class CSCServiceData(
val data: CSCData = CSCData(), val data: CSCData = CSCData(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null val connectionState: GattConnectionState? = null

View File

@@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.core.simpleSharedFlow
import no.nordicsemi.android.common.logger.NordicLogger import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.csc.data.CSCServicesData import no.nordicsemi.android.csc.data.CSCServiceData
import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData
@@ -61,7 +61,7 @@ class CSCRepository @Inject constructor(
private val _wheelSize = MutableStateFlow(WheelSizes.default) private val _wheelSize = MutableStateFlow(WheelSizes.default)
internal val wheelSize = _wheelSize.asStateFlow() internal val wheelSize = _wheelSize.asStateFlow()
private val _data = MutableStateFlow(CSCServicesData()) private val _data = MutableStateFlow(CSCServiceData())
internal val data = _data.asStateFlow() internal val data = _data.asStateFlow()
private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>() private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>()

View File

@@ -49,7 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.common.theme.view.RadioButtonGroup import no.nordicsemi.android.common.theme.view.RadioButtonGroup
import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCServicesData import no.nordicsemi.android.csc.data.CSCServiceData
import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData
import no.nordicsemi.android.kotlin.ble.profile.csc.WheelSize import no.nordicsemi.android.kotlin.ble.profile.csc.WheelSize
import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.ScreenSection
@@ -58,7 +58,7 @@ import no.nordicsemi.android.ui.view.dialog.FlowCanceled
import no.nordicsemi.android.ui.view.dialog.ItemSelectedResult import no.nordicsemi.android.ui.view.dialog.ItemSelectedResult
@Composable @Composable
internal fun CSCContentView(state: CSCServicesData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) { internal fun CSCContentView(state: CSCServiceData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) {
val showDialog = rememberSaveable { mutableStateOf(false) } val showDialog = rememberSaveable { mutableStateOf(false) }
if (showDialog.value) { if (showDialog.value) {
@@ -125,5 +125,5 @@ private fun SettingsSection(
@Preview @Preview
@Composable @Composable
private fun ConnectedPreview() { private fun ConnectedPreview() {
CSCContentView(CSCServicesData(), SpeedUnit.KM_H) { } CSCContentView(CSCServiceData(), SpeedUnit.KM_H) { }
} }

View File

@@ -31,7 +31,7 @@
package no.nordicsemi.android.csc.view package no.nordicsemi.android.csc.view
import no.nordicsemi.android.csc.data.CSCServicesData import no.nordicsemi.android.csc.data.CSCServiceData
internal data class CSCViewState( internal data class CSCViewState(
val speedUnit: SpeedUnit = SpeedUnit.M_S, val speedUnit: SpeedUnit = SpeedUnit.M_S,
@@ -41,6 +41,6 @@ internal data class CSCViewState(
internal sealed class CSCMangerState internal sealed class CSCMangerState
internal data class WorkingState(val result: CSCServicesData) : CSCMangerState() internal data class WorkingState(val result: CSCServiceData) : CSCMangerState()
internal object NoDeviceState : CSCMangerState() internal object NoDeviceState : CSCMangerState()

View File

@@ -40,14 +40,14 @@ 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.csc.R import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCServicesData import no.nordicsemi.android.csc.data.CSCServiceData
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 SensorsReadingView(state: CSCServicesData, speedUnit: SpeedUnit) { internal fun SensorsReadingView(state: CSCServiceData, speedUnit: SpeedUnit) {
val csc = state.data val csc = state.data
ScreenSection { ScreenSection {
SectionTitle(resId = R.drawable.ic_records, title = "Records") SectionTitle(resId = R.drawable.ic_records, title = "Records")
@@ -80,5 +80,5 @@ internal fun SensorsReadingView(state: CSCServicesData, speedUnit: SpeedUnit) {
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
SensorsReadingView(CSCServicesData(), SpeedUnit.KM_H) SensorsReadingView(CSCServiceData(), SpeedUnit.KM_H)
} }

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(libs.nordic.navigation) implementation(libs.nordic.navigation)
implementation(libs.nordic.uiscanner) implementation(libs.nordic.uiscanner)
implementation(libs.nordic.uilogger) implementation(libs.nordic.uilogger)
implementation(libs.nordic.core)
implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.compose.material.iconsExtended)

View File

@@ -1,135 +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.hrs.data
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
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.hr.BodySensorLocationResponse
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspendForValidResponse
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-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 HRSManager(
context: Context,
private val scope: CoroutineScope,
private val logger: NordicLogger
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var heartRateCharacteristic: BluetoothGattCharacteristic? = null
private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null
private val data = MutableStateFlow(HRSData())
val dataHolder = ConnectionObserverAdapter<HRSData>()
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 HeartRateManagerCallback()
}
private inner class HeartRateManagerCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
scope.launchWithCatch {
val readData = readCharacteristic(bodySensorLocationCharacteristic)
.suspendForValidResponse<BodySensorLocationResponse>()
data.value = data.value.copy(sensorLocation = readData.sensorLocation)
}
setNotificationCallback(heartRateCharacteristic).asValidResponseFlow<HeartRateMeasurementResponse>()
.onEach {
val result = data.value.heartRates.toMutableList().apply {
add(it.heartRate)
}
data.tryEmit(data.value.copy(heartRates = result))
}.launchIn(scope)
enableNotifications(heartRateCharacteristic).enqueue()
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>().onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue()
}
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
gatt.getService(HRS_SERVICE_UUID)?.run {
heartRateCharacteristic = getCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID)
bodySensorLocationCharacteristic = getCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)
}
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return heartRateCharacteristic != null
}
override fun onServicesInvalidated() {
bodySensorLocationCharacteristic = null
heartRateCharacteristic = null
batteryLevelCharacteristic = null
}
}
}

View File

@@ -31,8 +31,14 @@
package no.nordicsemi.android.hrs.data package no.nordicsemi.android.hrs.data
internal data class HRSData( import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
val heartRates: List<Int> = emptyList(), import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSData
internal data class HRSServiceData(
val data: List<HRSData> = emptyList(),
val bodySensorLocation: Int? = null,
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val sensorLocation: Int = 0, val connectionState: GattConnectionState? = null
) ) {
val heartRates = data.map { it.heartRate }
}

View File

@@ -33,22 +33,18 @@ package no.nordicsemi.android.hrs.service
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.common.core.simpleSharedFlow
import kotlinx.coroutines.launch
import no.nordicsemi.android.common.logger.NordicLogger import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.common.logger.NordicLoggerFactory import no.nordicsemi.android.hrs.data.HRSServiceData
import no.nordicsemi.android.hrs.data.HRSData
import no.nordicsemi.android.hrs.data.HRSManager
import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.service.IdleResult import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSData
import no.nordicsemi.android.service.DisconnectAndStopEvent
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.ui.view.StringConst
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -56,57 +52,43 @@ import javax.inject.Singleton
class HRSRepository @Inject constructor( class HRSRepository @Inject constructor(
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager
private val loggerFactory: NordicLoggerFactory,
private val stringConst: StringConst
) { ) {
private var manager: HRSManager? = null
private var logger: NordicLogger? = null private var logger: NordicLogger? = null
private val _data = MutableStateFlow<BleManagerResult<HRSData>>(IdleResult()) private val _data = MutableStateFlow(HRSServiceData())
internal val data = _data.asStateFlow() internal val data = _data.asStateFlow()
val isRunning = data.map { it.isRunning() } private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>()
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } internal val stopEvent = _stopEvent.asSharedFlow()
val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED }
fun launch(device: ServerDevice) { fun launch(device: ServerDevice) {
serviceManager.startService(HRSService::class.java, device) serviceManager.startService(HRSService::class.java, device)
} }
fun start(device: ServerDevice, scope: CoroutineScope) { fun onConnectionStateChanged(connectionState: GattConnectionState?) {
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "HRS", device.address).also { _data.value = _data.value.copy(connectionState = connectionState)
logger = it }
}
val manager = HRSManager(context, scope, createdLogger)
this.manager = manager
manager.dataHolder.status.onEach { fun onHRSDataChanged(data: HRSData) {
_data.value = it _data.value = _data.value.copy(data = _data.value.data + data)
}.launchIn(scope) }
scope.launch { fun onBodySensorLocationChanged(bodySensorLocation: Int) {
manager.start(device) _data.value = _data.value.copy(bodySensorLocation = bodySensorLocation)
} }
fun onBatteryLevelChanged(batteryLevel: Int) {
_data.value = _data.value.copy(batteryLevel = batteryLevel)
} }
fun openLogger() { fun openLogger() {
NordicLogger.launch(context, logger) NordicLogger.launch(context, logger)
} }
private suspend fun HRSManager.start(device: ServerDevice) {
// try {
// connect(device.device)
// .useAutoConnect(false)
// .retry(3, 100)
// .suspend()
// } catch (e: Exception) {
// e.printStackTrace()
// }
}
fun release() { fun release() {
manager?.disconnect()?.enqueue() _stopEvent.tryEmit(DisconnectAndStopEvent())
logger = null
manager = null
} }
} }

View File

@@ -31,33 +31,100 @@
package no.nordicsemi.android.hrs.service package no.nordicsemi.android.hrs.service
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
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.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
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.hrs.BodySensorLocationParser
import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSDataParser
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import java.util.*
import javax.inject.Inject import javax.inject.Inject
val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-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")
@AndroidEntryPoint @AndroidEntryPoint
internal class HRSService : NotificationService() { internal class HRSService : NotificationService() {
@Inject @Inject
lateinit var repository: HRSRepository lateinit var repository: HRSRepository
private lateinit var client: BleGattClient
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<ServerDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) startGattClient(device)
repository.hasBeenDisconnected.onEach { repository.stopEvent
if (it) stopSelf() .onEach { disconnect() }
}.launchIn(lifecycleScope) .launchIn(lifecycleScope)
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch {
client = blinkyDevice.connect(this@HRSService)
client.connectionState
.onEach { repository.onConnectionStateChanged(it) }
.filterNotNull()
.onEach { stopIfDisconnected(it) }
.launchIn(lifecycleScope)
client.services
.filterNotNull()
.onEach { configureGatt(it) }
.launchIn(lifecycleScope)
}
private suspend fun configureGatt(services: BleGattServices) {
val htsService = services.findService(HRS_SERVICE_UUID)!!
val htsMeasurementCharacteristic = htsService.findCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID)!!
val bodySensorLocationCharacteristic = htsService.findCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)!!
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
val bodySensorLocation = bodySensorLocationCharacteristic.read()
BodySensorLocationParser.parse(bodySensorLocation)?.let { repository.onBodySensorLocationChanged(it) }
batteryLevelCharacteristic.getNotifications()
.mapNotNull { BatteryLevelParser.parse(it) }
.onEach { repository.onBatteryLevelChanged(it) }
.launchIn(lifecycleScope)
htsMeasurementCharacteristic.getNotifications()
.mapNotNull { HRSDataParser.parse(it) }
.onEach { repository.onHRSDataChanged(it) }
.launchIn(lifecycleScope)
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
stopSelf()
}
}
private fun disconnect() {
client.disconnect()
}
} }

View File

@@ -46,13 +46,13 @@ 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.hrs.R import no.nordicsemi.android.hrs.R
import no.nordicsemi.android.hrs.data.HRSData import no.nordicsemi.android.hrs.data.HRSServiceData
import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.BatteryLevelView
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 HRSContentView(state: HRSData, zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) { internal fun HRSContentView(state: HRSServiceData, zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
@@ -102,5 +102,5 @@ private fun Menu(zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) {
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
HRSContentView(state = HRSData(), zoomIn = false) { } HRSContentView(state = HRSServiceData(), zoomIn = false) { }
} }

View File

@@ -48,15 +48,7 @@ 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.hrs.R import no.nordicsemi.android.hrs.R
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
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,14 @@ fun HRSScreen() {
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
when (state) { when (state.hrsManagerState) {
NoDeviceState -> DeviceConnectingView() NoDeviceState -> DeviceConnectingView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.hrsManagerState.result.connectionState) {
is IdleResult, null,
is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) }
is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTED,
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } GattConnectionState.STATE_CONNECTED -> HRSContentView(state.hrsManagerState.result, state.zoomIn) { viewModel.onEvent(it) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> HRSContentView(state.result.data, state.zoomIn) { viewModel.onEvent(it) }
} }
} }
} }
@@ -97,15 +86,11 @@ fun HRSScreen() {
@Composable @Composable
private fun AppBar(state: HRSViewState, navigateUp: () -> Unit, viewModel: HRSViewModel) { private fun AppBar(state: HRSViewState, navigateUp: () -> Unit, viewModel: HRSViewModel) {
val toolbarName = (state as? WorkingState)?.let { if (state.deviceName?.isNotBlank() == true) {
(it.result as? DeviceHolder)?.deviceName() LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent) viewModel.onEvent(OpenLoggerEvent)
} }
} else {
BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp)
} }
} }

View File

@@ -31,14 +31,16 @@
package no.nordicsemi.android.hrs.view package no.nordicsemi.android.hrs.view
import no.nordicsemi.android.hrs.data.HRSData import no.nordicsemi.android.hrs.data.HRSServiceData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class HRSViewState internal data class HRSViewState(
internal data class WorkingState(
val result: BleManagerResult<HRSData>,
val zoomIn: Boolean = false, val zoomIn: Boolean = false,
) : HRSViewState() val hrsManagerState: HRSManagerState = NoDeviceState,
val deviceName: String? = null
)
internal object NoDeviceState : HRSViewState() internal sealed class HRSManagerState
internal data class WorkingState(val result: HRSServiceData) : HRSManagerState()
internal object NoDeviceState : HRSManagerState()

View File

@@ -47,7 +47,7 @@ import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
import no.nordicsemi.android.hrs.data.HRSData import no.nordicsemi.android.hrs.data.HRSServiceData
private const val X_AXIS_ELEMENTS_COUNT = 40f private const val X_AXIS_ELEMENTS_COUNT = 40f
@@ -55,7 +55,7 @@ private const val AXIS_MIN = 0
private const val AXIS_MAX = 300 private const val AXIS_MAX = 300
@Composable @Composable
internal fun LineChartView(state: HRSData, zoomIn: Boolean,) { internal fun LineChartView(state: HRSServiceData, zoomIn: Boolean,) {
val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed() val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed()
val isSystemInDarkTheme = isSystemInDarkTheme() val isSystemInDarkTheme = isSystemInDarkTheme()
AndroidView( AndroidView(
@@ -119,8 +119,8 @@ internal fun createLineChartView(
val entries = points.mapIndexed { i, v -> val entries = points.mapIndexed { i, v ->
Entry(-i.toFloat(), v.toFloat()) Entry(-i.toFloat(), v.toFloat())
}.reversed() }.reversed()
// create a dataset and give it a type
// create a dataset and give it a type
if (data != null && data.dataSetCount > 0) { if (data != null && data.dataSetCount > 0) {
val set1 = data!!.getDataSetByIndex(0) as LineDataSet val set1 = data!!.getDataSetByIndex(0) as LineDataSet
set1.values = entries set1.values = entries
@@ -133,13 +133,9 @@ internal fun createLineChartView(
set1.setDrawIcons(false) set1.setDrawIcons(false)
set1.setDrawValues(false) set1.setDrawValues(false)
// draw dashed line
// draw dashed line // draw dashed line
set1.enableDashedLine(10f, 5f, 0f) set1.enableDashedLine(10f, 5f, 0f)
// black lines and points
// black lines and points // black lines and points
if (isDarkTheme) { if (isDarkTheme) {
set1.color = Color.WHITE set1.color = Color.WHITE
@@ -149,31 +145,21 @@ internal fun createLineChartView(
set1.setCircleColor(Color.BLACK) set1.setCircleColor(Color.BLACK)
} }
// line thickness and point size
// line thickness and point size // line thickness and point size
set1.lineWidth = 1f set1.lineWidth = 1f
set1.circleRadius = 3f set1.circleRadius = 3f
// draw points as solid circles
// draw points as solid circles // draw points as solid circles
set1.setDrawCircleHole(false) set1.setDrawCircleHole(false)
// customize legend entry
// customize legend entry // customize legend entry
set1.formLineWidth = 1f set1.formLineWidth = 1f
set1.formLineDashEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f) set1.formLineDashEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
set1.formSize = 15f set1.formSize = 15f
// text size of values
// text size of values // text size of values
set1.valueTextSize = 9f set1.valueTextSize = 9f
// draw selection line as dashed
// draw selection line as dashed // draw selection line as dashed
set1.enableDashedHighlightLine(10f, 5f, 0f) set1.enableDashedHighlightLine(10f, 5f, 0f)
@@ -183,8 +169,6 @@ internal fun createLineChartView(
// create a data object with the data sets // create a data object with the data sets
val data = LineData(dataSets) val data = LineData(dataSets)
// set data
// set data // set data
setData(data) setData(data)
} }

View File

@@ -46,18 +46,17 @@ import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent import no.nordicsemi.android.analytics.ProfileConnectedEvent
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.hrs.data.HRS_SERVICE_UUID
import no.nordicsemi.android.hrs.service.HRSRepository import no.nordicsemi.android.hrs.service.HRSRepository
import no.nordicsemi.android.hrs.service.HRS_SERVICE_UUID
import no.nordicsemi.android.hrs.view.DisconnectEvent import no.nordicsemi.android.hrs.view.DisconnectEvent
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
import no.nordicsemi.android.hrs.view.HRSViewState import no.nordicsemi.android.hrs.view.HRSViewState
import no.nordicsemi.android.hrs.view.NavigateUpEvent import no.nordicsemi.android.hrs.view.NavigateUpEvent
import no.nordicsemi.android.hrs.view.NoDeviceState
import no.nordicsemi.android.hrs.view.OpenLoggerEvent import no.nordicsemi.android.hrs.view.OpenLoggerEvent
import no.nordicsemi.android.hrs.view.SwitchZoomEvent import no.nordicsemi.android.hrs.view.SwitchZoomEvent
import no.nordicsemi.android.hrs.view.WorkingState import no.nordicsemi.android.hrs.view.WorkingState
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.data.GattConnectionState
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import javax.inject.Inject import javax.inject.Inject
@@ -68,7 +67,7 @@ internal class HRSViewModel @Inject constructor(
private val analytics: AppAnalytics private val analytics: AppAnalytics
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<HRSViewState>(NoDeviceState) private val _state = MutableStateFlow(HRSViewState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
init { init {
@@ -79,10 +78,9 @@ internal class HRSViewModel @Inject constructor(
} }
repository.data.onEach { repository.data.onEach {
val zoomIn = (_state.value as? WorkingState)?.zoomIn ?: false _state.value = _state.value.copy(hrsManagerState = WorkingState(it))
_state.value = WorkingState(it, zoomIn)
(it as? ConnectedResult)?.let { if (it.connectionState == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.HRS)) analytics.logEvent(ProfileConnectedEvent(Profile.HRS))
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@@ -113,9 +111,7 @@ internal class HRSViewModel @Inject constructor(
} }
private fun onZoomButtonClicked() { private fun onZoomButtonClicked() {
(_state.value as? WorkingState)?.let { _state.value = _state.value.copy(zoomIn = !_state.value.zoomIn)
_state.value = it.copy(zoomIn = !it.zoomIn)
}
} }
private fun disconnect() { private fun disconnect() {

View File

@@ -34,7 +34,7 @@ package no.nordicsemi.android.hts.data
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData
internal data class HTSServicesData( internal data class HTSServiceData(
val data: HTSData = HTSData(), val data: HTSData = HTSData(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null val connectionState: GattConnectionState? = null

View File

@@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.core.simpleSharedFlow
import no.nordicsemi.android.common.logger.NordicLogger import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.hts.data.HTSServicesData import no.nordicsemi.android.hts.data.HTSServiceData
import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData
@@ -56,7 +56,7 @@ class HTSRepository @Inject constructor(
) { ) {
private var logger: NordicLogger? = null private var logger: NordicLogger? = null
private val _data = MutableStateFlow(HTSServicesData()) private val _data = MutableStateFlow(HTSServiceData())
internal val data = _data.asStateFlow() internal val data = _data.asStateFlow()
private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>() private val _stopEvent = simpleSharedFlow<DisconnectAndStopEvent>()

View File

@@ -45,14 +45,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.common.theme.view.RadioButtonGroup import no.nordicsemi.android.common.theme.view.RadioButtonGroup
import no.nordicsemi.android.hts.R import no.nordicsemi.android.hts.R
import no.nordicsemi.android.hts.data.HTSServicesData import no.nordicsemi.android.hts.data.HTSServiceData
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 HTSContentView(state: HTSServicesData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) { internal fun HTSContentView(state: HTSServiceData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@@ -99,5 +99,5 @@ internal fun HTSContentView(state: HTSServicesData, temperatureUnit: Temperature
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
HTSContentView(state = HTSServicesData(), TemperatureUnit.CELSIUS) { } HTSContentView(state = HTSServiceData(), TemperatureUnit.CELSIUS) { }
} }

View File

@@ -31,7 +31,7 @@
package no.nordicsemi.android.hts.view package no.nordicsemi.android.hts.view
import no.nordicsemi.android.hts.data.HTSServicesData import no.nordicsemi.android.hts.data.HTSServiceData
internal data class HTSViewState( internal data class HTSViewState(
val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS,
@@ -41,6 +41,6 @@ internal data class HTSViewState(
internal sealed class HTSManagerState internal sealed class HTSManagerState
internal data class WorkingState(val result: HTSServicesData) : HTSManagerState() internal data class WorkingState(val result: HTSServiceData) : HTSManagerState()
internal object NoDeviceState : HTSManagerState() internal object NoDeviceState : HTSManagerState()