Add RSCS profile

This commit is contained in:
Sylwester Zielinski
2023-03-09 17:21:44 +01:00
parent 84eb61d59f
commit e10ebcf4b5
12 changed files with 159 additions and 235 deletions

View File

@@ -97,10 +97,15 @@ internal class HRSViewModel @Inject constructor(
private fun handleResult(result: NavigationResult<ServerDevice>) { private fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) { when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp() is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> repository.launch(result.value) is NavigationResult.Success -> onDeviceSelected(result.value)
} }
} }
private fun onDeviceSelected(device: ServerDevice) {
_state.value = _state.value.copy(deviceName = device.name)
repository.launch(device)
}
fun onEvent(event: HRSScreenViewEvent) { fun onEvent(event: HRSScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> disconnect() DisconnectEvent -> disconnect()

View File

@@ -55,6 +55,7 @@ dependencies {
implementation(libs.nordic.uiscanner) implementation(libs.nordic.uiscanner)
implementation(libs.nordic.navigation) implementation(libs.nordic.navigation)
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,126 +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.rscs.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.rsc.RunningSpeedAndCadenceMeasurementResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.common.logger.NordicLogger
import no.nordicsemi.android.service.ConnectionObserverAdapter
import java.util.*
val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB")
private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-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 RSCSManager internal constructor(
context: Context,
private val scope: CoroutineScope,
private val logger: NordicLogger
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private val data = MutableStateFlow(RSCSData())
val dataHolder = ConnectionObserverAdapter<RSCSData>()
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
}
private inner class RSCManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
setNotificationCallback(rscMeasurementCharacteristic).asValidResponseFlow<RunningSpeedAndCadenceMeasurementResponse>()
.onEach {
data.tryEmit(data.value.copy(
running = it.isRunning,
instantaneousCadence = it.instantaneousCadence,
instantaneousSpeed = it.instantaneousSpeed,
strideLength = it.strideLength,
totalDistance = it.totalDistance
))
}.launchIn(scope)
enableNotifications(rscMeasurementCharacteristic).enqueue()
setNotificationCallback(batteryLevelCharacteristic)
.asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue()
}
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
gatt.getService(RSCS_SERVICE_UUID)?.run {
rscMeasurementCharacteristic = getCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID)
}
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return rscMeasurementCharacteristic != null
}
override fun onServicesInvalidated() {
rscMeasurementCharacteristic = null
batteryLevelCharacteristic = null
}
}
override fun getGattCallback(): BleManagerGattCallback {
return RSCManagerGattCallback()
}
}

View File

@@ -31,37 +31,42 @@
package no.nordicsemi.android.rscs.data package no.nordicsemi.android.rscs.data
internal data class RSCSData( import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.rscs.RSCSData
import no.nordicsemi.android.rscs.R
internal data class RSCSServiceData(
val data: RSCSData = RSCSData(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val running: Boolean = false, val connectionState: GattConnectionState? = null
val instantaneousSpeed: Float = 1.0f,
val instantaneousCadence: Int = 0,
val strideLength: Int? = null,
val totalDistance: Long? = null
) { ) {
@Composable
fun displayActivity(): String { fun displayActivity(): String {
return if (running) { return if (data.running) {
"Running" stringResource(id = R.string.rscs_running)
} else { } else {
"Walking" stringResource(id = R.string.rscs_walking)
} }
} }
@Composable
fun displayPace(): String { fun displayPace(): String {
return "$instantaneousCadence min/km" return stringResource(id = R.string.rscs_speed, data.instantaneousSpeed)
} }
@Composable
fun displayCadence(): String { fun displayCadence(): String {
return "$instantaneousCadence RPM" return stringResource(id = R.string.rscs_rpm, data.instantaneousCadence)
} }
@Composable
fun displayNumberOfSteps(): String? { fun displayNumberOfSteps(): String? {
if (totalDistance == null || strideLength == null) { if (data.totalDistance == null || data.strideLength == null) {
return null return null
} }
val numberOfSteps = totalDistance/strideLength val numberOfSteps = data.totalDistance!! / data.strideLength!!.toLong()
return "Number of Steps $numberOfSteps" return stringResource(id = R.string.rscs_steps, numberOfSteps)
} }
} }

View File

@@ -33,22 +33,18 @@ package no.nordicsemi.android.rscs.repository
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.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.rscs.data.RSCSData import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.rscs.data.RSCSManager import no.nordicsemi.android.kotlin.ble.profile.rscs.RSCSData
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.rscs.data.RSCSServiceData
import no.nordicsemi.android.service.IdleResult 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,40 @@ import javax.inject.Singleton
class RSCSRepository @Inject constructor( class RSCSRepository @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: RSCSManager? = null
private var logger: NordicLogger? = null private var logger: NordicLogger? = null
private val _data = MutableStateFlow<BleManagerResult<RSCSData>>(IdleResult()) private val _data = MutableStateFlow(RSCSServiceData())
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(RSCSService::class.java, device) serviceManager.startService(RSCSService::class.java, device)
} }
fun start(device: ServerDevice, scope: CoroutineScope) { fun onConnectionStateChanged(connectionState: GattConnectionState?) {
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "RSCS", device.address).also { _data.value = _data.value.copy(connectionState = connectionState)
logger = it
} }
val manager = RSCSManager(context, scope, createdLogger)
this.manager = manager
manager.dataHolder.status.onEach { fun onRSCSDataChanged(data: RSCSData) {
_data.value = it _data.value = _data.value.copy(data = data)
}.launchIn(scope)
scope.launch {
manager.start(device)
} }
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 RSCSManager.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()
manager = null
logger = null logger = null
_stopEvent.tryEmit(DisconnectAndStopEvent())
} }
} }

View File

@@ -31,33 +31,94 @@
package no.nordicsemi.android.rscs.repository package no.nordicsemi.android.rscs.repository
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.rscs.RSCSDataParser
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 RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB")
private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-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 RSCSService : NotificationService() { internal class RSCSService : NotificationService() {
@Inject @Inject
lateinit var repository: RSCSRepository lateinit var repository: RSCSRepository
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@RSCSService)
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 rscsService = services.findService(RSCS_SERVICE_UUID)!!
val rscsMeasurementCharacteristic = rscsService.findCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID)!!
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
batteryLevelCharacteristic.getNotifications()
.mapNotNull { BatteryLevelParser.parse(it) }
.onEach { repository.onBatteryLevelChanged(it) }
.launchIn(lifecycleScope)
rscsMeasurementCharacteristic.getNotifications()
.mapNotNull { RSCSDataParser.parse(it) }
.onEach { repository.onRSCSDataChanged(it) }
.launchIn(lifecycleScope)
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
stopSelf()
}
}
private fun disconnect() {
client.disconnect()
}
} }

View File

@@ -43,11 +43,11 @@ 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.rscs.R import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.data.RSCSData import no.nordicsemi.android.rscs.data.RSCSServiceData
import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.BatteryLevelView
@Composable @Composable
internal fun RSCSContentView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) { internal fun RSCSContentView(state: RSCSServiceData, onEvent: (RSCScreenViewEvent) -> Unit) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
@@ -74,5 +74,5 @@ internal fun RSCSContentView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> U
@Preview @Preview
@Composable @Composable
private fun RSCSContentViewPreview() { private fun RSCSContentViewPreview() {
RSCSContentView(RSCSData()) { } RSCSContentView(RSCSServiceData()) { }
} }

View File

@@ -46,17 +46,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
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.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.rscs.R import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
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.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 RSCSScreen() {
.padding(16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
when (state) { when (state.rscsManagerState) {
NoDeviceState -> DeviceConnectingView() NoDeviceState -> DeviceConnectingView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.rscsManagerState.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 -> RSCSContentView(state.rscsManagerState.result) { viewModel.onEvent(it) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) }
is SuccessResult -> RSCSContentView(state.result.data) { viewModel.onEvent(it) }
} }
} }
} }
@@ -97,15 +86,11 @@ fun RSCSScreen() {
@Composable @Composable
private fun AppBar(state: RSCSViewState, navigateUp: () -> Unit, viewModel: RSCSViewModel) { private fun AppBar(state: RSCSViewState, navigateUp: () -> Unit, viewModel: RSCSViewModel) {
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.rscs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent) viewModel.onEvent(OpenLoggerEvent)
} }
} else {
BackIconAppBar(stringResource(id = R.string.rscs_title), navigateUp)
} }
} }

View File

@@ -31,11 +31,15 @@
package no.nordicsemi.android.rscs.view package no.nordicsemi.android.rscs.view
import no.nordicsemi.android.rscs.data.RSCSData import no.nordicsemi.android.rscs.data.RSCSServiceData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class RSCSViewState internal data class RSCSViewState(
val rscsManagerState: RSCSManagerState = NoDeviceState,
val deviceName: String? = null
)
internal data class WorkingState(val result: BleManagerResult<RSCSData>) : RSCSViewState() internal sealed class RSCSManagerState
internal object NoDeviceState : RSCSViewState() internal data class WorkingState(val result: RSCSServiceData) : RSCSManagerState()
internal object NoDeviceState : RSCSManagerState()

View File

@@ -39,13 +39,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.rscs.R import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.data.RSCSData import no.nordicsemi.android.rscs.data.RSCSServiceData
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: RSCSData) { internal fun SensorsReadingView(state: RSCSServiceData) {
ScreenSection { ScreenSection {
SectionTitle(resId = R.drawable.ic_records, title = "Records") SectionTitle(resId = R.drawable.ic_records, title = "Records")
@@ -66,5 +66,5 @@ internal fun SensorsReadingView(state: RSCSData) {
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
SensorsReadingView(RSCSData()) SensorsReadingView(RSCSServiceData())
} }

View File

@@ -47,16 +47,15 @@ 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.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.rscs.data.RSCS_SERVICE_UUID import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.rscs.repository.RSCSRepository import no.nordicsemi.android.rscs.repository.RSCSRepository
import no.nordicsemi.android.rscs.repository.RSCS_SERVICE_UUID
import no.nordicsemi.android.rscs.view.DisconnectEvent import no.nordicsemi.android.rscs.view.DisconnectEvent
import no.nordicsemi.android.rscs.view.NavigateUpEvent import no.nordicsemi.android.rscs.view.NavigateUpEvent
import no.nordicsemi.android.rscs.view.NoDeviceState
import no.nordicsemi.android.rscs.view.OpenLoggerEvent import no.nordicsemi.android.rscs.view.OpenLoggerEvent
import no.nordicsemi.android.rscs.view.RSCSViewState import no.nordicsemi.android.rscs.view.RSCSViewState
import no.nordicsemi.android.rscs.view.RSCScreenViewEvent import no.nordicsemi.android.rscs.view.RSCScreenViewEvent
import no.nordicsemi.android.rscs.view.WorkingState import no.nordicsemi.android.rscs.view.WorkingState
import no.nordicsemi.android.service.ConnectedResult
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import javax.inject.Inject import javax.inject.Inject
@@ -67,7 +66,7 @@ internal class RSCSViewModel @Inject constructor(
private val analytics: AppAnalytics private val analytics: AppAnalytics
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<RSCSViewState>(NoDeviceState) private val _state = MutableStateFlow(RSCSViewState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
init { init {
@@ -78,9 +77,9 @@ internal class RSCSViewModel @Inject constructor(
} }
repository.data.onEach { repository.data.onEach {
_state.value = WorkingState(it) _state.value = _state.value.copy(rscsManagerState = WorkingState(it))
(it as? ConnectedResult)?.let { if (it.connectionState == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.RSCS)) analytics.logEvent(ProfileConnectedEvent(Profile.RSCS))
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@@ -97,10 +96,15 @@ internal class RSCSViewModel @Inject constructor(
private fun handleResult(result: NavigationResult<ServerDevice>) { private fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) { when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp() is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> repository.launch(result.value) is NavigationResult.Success -> onDeviceSelected(result.value)
} }
} }
private fun onDeviceSelected(device: ServerDevice) {
_state.value = _state.value.copy(deviceName = device.name)
repository.launch(device)
}
fun onEvent(event: RSCScreenViewEvent) { fun onEvent(event: RSCScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> disconnect() DisconnectEvent -> disconnect()

View File

@@ -37,4 +37,10 @@
<string name="rscs_pace">Pace</string> <string name="rscs_pace">Pace</string>
<string name="rscs_cadence">Cadence</string> <string name="rscs_cadence">Cadence</string>
<string name="rscs_number_of_steps">Number of steps</string> <string name="rscs_number_of_steps">Number of steps</string>
<string name="rscs_walking">Walking</string>
<string name="rscs_running">Running</string>
<string name="rscs_speed">%.1f min/km</string>
<string name="rscs_rpm">%d RPM</string>
<string name="rscs_steps">Number of Steps %d</string>
</resources> </resources>