mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-21 00:14:24 +01:00
Change CSC module
This commit is contained in:
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.cgms.data.CGMRepository
|
import no.nordicsemi.android.cgms.data.CGMRepository
|
||||||
|
import no.nordicsemi.android.csc.data.CSCRepository
|
||||||
import no.nordicsemi.android.navigation.NavigationManager
|
import no.nordicsemi.android.navigation.NavigationManager
|
||||||
import no.nordicsemi.android.nrftoolbox.ProfileDestination
|
import no.nordicsemi.android.nrftoolbox.ProfileDestination
|
||||||
import no.nordicsemi.android.nrftoolbox.view.HomeViewState
|
import no.nordicsemi.android.nrftoolbox.view.HomeViewState
|
||||||
@@ -16,7 +17,8 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(
|
class HomeViewModel @Inject constructor(
|
||||||
private val navigationManager: NavigationManager,
|
private val navigationManager: NavigationManager,
|
||||||
private val cgmRepository: CGMRepository
|
cgmRepository: CGMRepository,
|
||||||
|
cscRepository: CSCRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(HomeViewState())
|
private val _state = MutableStateFlow(HomeViewState())
|
||||||
@@ -26,6 +28,10 @@ class HomeViewModel @Inject constructor(
|
|||||||
cgmRepository.isRunning.onEach {
|
cgmRepository.isRunning.onEach {
|
||||||
_state.value = _state.value.copy(isCGMModuleRunning = it)
|
_state.value = _state.value.copy(isCGMModuleRunning = it)
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
cscRepository.isRunning.onEach {
|
||||||
|
_state.value = _state.value.copy(isCSCModuleRunning = it)
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openProfile(destination: ProfileDestination) {
|
fun openProfile(destination: ProfileDestination) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class CGMRepository @Inject constructor(
|
|||||||
|
|
||||||
fun start(device: BluetoothDevice, scope: CoroutineScope) {
|
fun start(device: BluetoothDevice, scope: CoroutineScope) {
|
||||||
val manager = CGMManager(context, scope)
|
val manager = CGMManager(context, scope)
|
||||||
|
this.manager = manager
|
||||||
|
|
||||||
manager.dataHolder.status.onEach {
|
manager.dataHolder.status.onEach {
|
||||||
_data.value = it
|
_data.value = it
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import no.nordicsemi.android.cgms.view.*
|
|||||||
import no.nordicsemi.android.navigation.*
|
import no.nordicsemi.android.navigation.*
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import no.nordicsemi.android.utils.getDevice
|
import no.nordicsemi.android.utils.getDevice
|
||||||
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
|
||||||
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ internal class CGMScreenViewModel @Inject constructor(
|
|||||||
private fun handleArgs(args: DestinationResult) {
|
private fun handleArgs(args: DestinationResult) {
|
||||||
when (args) {
|
when (args) {
|
||||||
is CancelDestinationResult -> navigationManager.navigateUp()
|
is CancelDestinationResult -> navigationManager.navigateUp()
|
||||||
is SuccessDestinationResult -> connectDevice(args.getDevice())
|
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,10 +70,6 @@ internal class CGMScreenViewModel @Inject constructor(
|
|||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectDevice(deviceHolder: DiscoveredBluetoothDevice) {
|
|
||||||
repository.launch(deviceHolder.device)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun disconnect() {
|
private fun disconnect() {
|
||||||
repository.release()
|
repository.release()
|
||||||
navigationManager.navigateUp()
|
navigationManager.navigateUp()
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
package no.nordicsemi.android.csc.data
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
|
||||||
import no.nordicsemi.android.material.you.RadioButtonItem
|
|
||||||
import no.nordicsemi.android.material.you.RadioGroupViewEntity
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val DISPLAY_M_S = "m/s"
|
|
||||||
private const val DISPLAY_KM_H = "km/h"
|
|
||||||
private const val DISPLAY_MPH = "mph"
|
|
||||||
|
|
||||||
internal data class CSCData(
|
internal data class CSCData(
|
||||||
val scanDevices: Boolean = false,
|
val scanDevices: Boolean = false,
|
||||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
|
||||||
val speed: Float = 0f,
|
val speed: Float = 0f,
|
||||||
val cadence: Float = 0f,
|
val cadence: Float = 0f,
|
||||||
val distance: Float = 0f,
|
val distance: Float = 0f,
|
||||||
@@ -19,70 +9,4 @@ internal data class CSCData(
|
|||||||
val gearRatio: Float = 0f,
|
val gearRatio: Float = 0f,
|
||||||
val batteryLevel: Int = 0,
|
val batteryLevel: Int = 0,
|
||||||
val wheelSize: WheelSize = WheelSize()
|
val wheelSize: WheelSize = WheelSize()
|
||||||
) {
|
)
|
||||||
|
|
||||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
|
||||||
SpeedUnit.M_S -> speed
|
|
||||||
SpeedUnit.KM_H -> speed * 3.6f
|
|
||||||
SpeedUnit.MPH -> speed * 2.2369f
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displaySpeed(): String {
|
|
||||||
return when (selectedSpeedUnit) {
|
|
||||||
SpeedUnit.M_S -> String.format("%.1f m/s", speedWithUnit)
|
|
||||||
SpeedUnit.KM_H -> String.format("%.1f km/h", speedWithUnit)
|
|
||||||
SpeedUnit.MPH -> String.format("%.1f mph", speedWithUnit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayCadence(): String {
|
|
||||||
return String.format("%.0f RPM", cadence)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayDistance(): String {
|
|
||||||
return when (selectedSpeedUnit) {
|
|
||||||
SpeedUnit.M_S -> String.format("%.0f m", distance)
|
|
||||||
SpeedUnit.KM_H -> String.format("%.0f m", distance)
|
|
||||||
SpeedUnit.MPH -> String.format("%.0f yd", distance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayTotalDistance(): String {
|
|
||||||
return when (selectedSpeedUnit) {
|
|
||||||
SpeedUnit.M_S -> String.format("%.2f km", totalDistance)
|
|
||||||
SpeedUnit.KM_H -> String.format("%.2f km", totalDistance)
|
|
||||||
SpeedUnit.MPH -> String.format("%.2f mile", totalDistance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayGearRatio(): String {
|
|
||||||
return String.format(Locale.US, "%.1f", gearRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSpeedUnit(label: String): SpeedUnit {
|
|
||||||
return when (label) {
|
|
||||||
DISPLAY_KM_H -> SpeedUnit.KM_H
|
|
||||||
DISPLAY_M_S -> SpeedUnit.M_S
|
|
||||||
DISPLAY_MPH -> SpeedUnit.MPH
|
|
||||||
else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $label")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun temperatureSettingsItems(): RadioGroupViewEntity {
|
|
||||||
return RadioGroupViewEntity(
|
|
||||||
SpeedUnit.values().map { createRadioButtonItem(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createRadioButtonItem(unit: SpeedUnit): RadioButtonItem {
|
|
||||||
return RadioButtonItem(displayTemperature(unit), unit == selectedSpeedUnit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun displayTemperature(unit: SpeedUnit): String {
|
|
||||||
return when (unit) {
|
|
||||||
SpeedUnit.KM_H -> DISPLAY_KM_H
|
|
||||||
SpeedUnit.M_S -> DISPLAY_M_S
|
|
||||||
SpeedUnit.MPH -> DISPLAY_MPH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,68 +1,74 @@
|
|||||||
package no.nordicsemi.android.csc.data
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import android.bluetooth.BluetoothDevice
|
||||||
import kotlinx.coroutines.flow.*
|
import android.content.Context
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import no.nordicsemi.android.service.BleManagerStatus
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import no.nordicsemi.android.ble.ktx.suspend
|
||||||
|
import no.nordicsemi.android.csc.repository.CSCManager
|
||||||
|
import no.nordicsemi.android.csc.repository.CSCService
|
||||||
|
import no.nordicsemi.android.service.BleManagerResult
|
||||||
|
import no.nordicsemi.android.service.ConnectingResult
|
||||||
|
import no.nordicsemi.android.service.ServiceManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class CSCRepository @Inject constructor() {
|
class CSCRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val serviceManager: ServiceManager,
|
||||||
|
) {
|
||||||
|
private var manager: CSCManager? = null
|
||||||
|
|
||||||
private val _data = MutableStateFlow(CSCData())
|
private val _data = MutableStateFlow<BleManagerResult<CSCData>>(ConnectingResult())
|
||||||
val data: StateFlow<CSCData> = _data.asStateFlow()
|
internal val data = _data.asStateFlow()
|
||||||
|
|
||||||
private val _command = MutableSharedFlow<CSCServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
private val _isRunning = MutableStateFlow(false)
|
||||||
val command = _command.asSharedFlow()
|
val isRunning = _isRunning.asStateFlow()
|
||||||
|
|
||||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
fun launch(device: BluetoothDevice) {
|
||||||
val status = _status.asStateFlow()
|
serviceManager.startService(CSCService::class.java, device)
|
||||||
|
|
||||||
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
|
|
||||||
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNewDistance(
|
fun start(device: BluetoothDevice, scope: CoroutineScope) {
|
||||||
totalDistance: Float,
|
val manager = CSCManager(context, scope)
|
||||||
distance: Float,
|
this.manager = manager
|
||||||
speed: Float,
|
|
||||||
wheelSize: WheelSize
|
|
||||||
) {
|
|
||||||
_data.tryEmit(_data.value.copy(
|
|
||||||
totalDistance = totalDistance,
|
|
||||||
distance = distance,
|
|
||||||
speed = speed,
|
|
||||||
wheelSize = wheelSize
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setNewCrankCadence(
|
manager.dataHolder.status.onEach {
|
||||||
crankCadence: Float,
|
_data.value = it
|
||||||
gearRatio: Float,
|
}.launchIn(scope)
|
||||||
wheelSize: WheelSize
|
|
||||||
) {
|
|
||||||
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBatteryLevel(batteryLevel: Int) {
|
scope.launch {
|
||||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
manager.start(device)
|
||||||
}
|
|
||||||
|
|
||||||
fun sendNewServiceCommand(workingMode: CSCServiceCommand) {
|
|
||||||
if (_command.subscriptionCount.value > 0) {
|
|
||||||
_command.tryEmit(workingMode)
|
|
||||||
} else {
|
|
||||||
_status.tryEmit(BleManagerStatus.DISCONNECTED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNewStatus(status: BleManagerStatus) {
|
fun setWheelSize(wheelSize: WheelSize) {
|
||||||
_status.value = status
|
manager?.setWheelSize(wheelSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
private suspend fun CSCManager.start(device: BluetoothDevice) {
|
||||||
_status.value = BleManagerStatus.CONNECTING
|
try {
|
||||||
_data.tryEmit(CSCData())
|
connect(device)
|
||||||
|
.useAutoConnect(false)
|
||||||
|
.retry(3, 100)
|
||||||
|
.suspend()
|
||||||
|
_isRunning.value = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
serviceManager.stopService(CSCService::class.java)
|
||||||
|
manager?.disconnect()?.enqueue()
|
||||||
|
manager = null
|
||||||
|
_isRunning.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,17 +21,15 @@
|
|||||||
*/
|
*/
|
||||||
package no.nordicsemi.android.csc.repository
|
package no.nordicsemi.android.csc.repository
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import no.nordicsemi.android.ble.BleManager
|
import no.nordicsemi.android.ble.BleManager
|
||||||
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
|
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
|
||||||
@@ -40,9 +38,8 @@ import no.nordicsemi.android.ble.ktx.asValidResponseFlow
|
|||||||
import no.nordicsemi.android.ble.ktx.suspend
|
import no.nordicsemi.android.ble.ktx.suspend
|
||||||
import no.nordicsemi.android.csc.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
import no.nordicsemi.android.csc.data.WheelSize
|
import no.nordicsemi.android.csc.data.WheelSize
|
||||||
import no.nordicsemi.android.service.*
|
import no.nordicsemi.android.service.ConnectionObserverAdapter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
|
||||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||||
@@ -50,35 +47,6 @@ private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000
|
|||||||
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-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")
|
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
internal class CSCRepo @Inject constructor(
|
|
||||||
@ApplicationContext
|
|
||||||
private val context: Context,
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<CSCData>> = callbackFlow {
|
|
||||||
val scope = CoroutineScope(coroutineContext)
|
|
||||||
val manager = CSCManager(context, scope)
|
|
||||||
|
|
||||||
manager.dataHolder.status.onEach {
|
|
||||||
trySend(it)
|
|
||||||
}.launchIn(scope)
|
|
||||||
|
|
||||||
scope.launch {
|
|
||||||
manager.connect(device)
|
|
||||||
.useAutoConnect(false)
|
|
||||||
.retry(3, 100)
|
|
||||||
.suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
|
||||||
scope.launch {
|
|
||||||
manager.disconnect().suspend()
|
|
||||||
}
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class CSCManager(
|
internal class CSCManager(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val scope: CoroutineScope,
|
private val scope: CoroutineScope,
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
package no.nordicsemi.android.csc.repository
|
package no.nordicsemi.android.csc.repository
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import no.nordicsemi.android.csc.data.CSCRepository
|
import no.nordicsemi.android.csc.data.CSCRepository
|
||||||
import no.nordicsemi.android.csc.data.DisconnectCommand
|
import no.nordicsemi.android.service.DEVICE_DATA
|
||||||
import no.nordicsemi.android.csc.data.SetWheelSizeCommand
|
import no.nordicsemi.android.service.NotificationService
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class CSCService : ForegroundBleService() {
|
internal class CSCService : NotificationService() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var repository: CSCRepository
|
lateinit var repository: CSCRepository
|
||||||
|
|
||||||
override val manager: CSCManager by lazy { CSCManager(this, scope) }
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
override fun onCreate() {
|
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
repository.command.onEach {
|
repository.start(device, lifecycleScope)
|
||||||
when (it) {
|
|
||||||
DisconnectCommand -> stopSelf()
|
return START_REDELIVER_INTENT
|
||||||
is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize)
|
|
||||||
}.exhaustive
|
|
||||||
}.launchIn(scope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
|||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun CSCContentView(state: CSCData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
val showDialog = rememberSaveable { mutableStateOf(false) }
|
val showDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showDialog.value) {
|
if (showDialog.value) {
|
||||||
@@ -51,11 +51,11 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
SettingsSection(state, onEvent) { showDialog.value = true }
|
SettingsSection(state, speedUnit, onEvent) { showDialog.value = true }
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
SensorsReadingView(state = state)
|
SensorsReadingView(state = state, speedUnit)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -70,6 +70,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSection(
|
private fun SettingsSection(
|
||||||
state: CSCData,
|
state: CSCData,
|
||||||
|
speedUnit: SpeedUnit,
|
||||||
onEvent: (CSCViewEvent) -> Unit,
|
onEvent: (CSCViewEvent) -> Unit,
|
||||||
onWheelButtonClick: () -> Unit,
|
onWheelButtonClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -86,8 +87,8 @@ private fun SettingsSection(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
RadioButtonGroup(viewEntity = state.temperatureSettingsItems()) {
|
RadioButtonGroup(viewEntity = speedUnit.temperatureSettingsItems()) {
|
||||||
onEvent(OnSelectedSpeedUnitSelected(state.getSpeedUnit(it.label)))
|
onEvent(OnSelectedSpeedUnitSelected(it.label.toSpeedUnit()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,5 +97,5 @@ private fun SettingsSection(
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectedPreview() {
|
private fun ConnectedPreview() {
|
||||||
CSCContentView(CSCData()) { }
|
CSCContentView(CSCData(), SpeedUnit.KM_H) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
|
import no.nordicsemi.android.material.you.RadioButtonItem
|
||||||
|
import no.nordicsemi.android.material.you.RadioGroupViewEntity
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val DISPLAY_M_S = "m/s"
|
||||||
|
private const val DISPLAY_KM_H = "km/h"
|
||||||
|
private const val DISPLAY_MPH = "mph"
|
||||||
|
|
||||||
|
internal fun CSCData.speedWithSpeedUnit(speedUnit: SpeedUnit): Float {
|
||||||
|
return when (speedUnit) {
|
||||||
|
SpeedUnit.M_S -> speed
|
||||||
|
SpeedUnit.KM_H -> speed * 3.6f
|
||||||
|
SpeedUnit.MPH -> speed * 2.2369f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CSCData.displaySpeed(speedUnit: SpeedUnit): String {
|
||||||
|
val speedWithUnit = speedWithSpeedUnit(speedUnit)
|
||||||
|
return when (speedUnit) {
|
||||||
|
SpeedUnit.M_S -> String.format("%.1f m/s", speedWithUnit)
|
||||||
|
SpeedUnit.KM_H -> String.format("%.1f km/h", speedWithUnit)
|
||||||
|
SpeedUnit.MPH -> String.format("%.1f mph", speedWithUnit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CSCData.displayCadence(): String {
|
||||||
|
return String.format("%.0f RPM", cadence)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CSCData.displayDistance(speedUnit: SpeedUnit): String {
|
||||||
|
return when (speedUnit) {
|
||||||
|
SpeedUnit.M_S -> String.format("%.0f m", distance)
|
||||||
|
SpeedUnit.KM_H -> String.format("%.0f m", distance)
|
||||||
|
SpeedUnit.MPH -> String.format("%.0f yd", distance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CSCData.displayTotalDistance(speedUnit: SpeedUnit): String {
|
||||||
|
return when (speedUnit) {
|
||||||
|
SpeedUnit.M_S -> String.format("%.2f km", totalDistance)
|
||||||
|
SpeedUnit.KM_H -> String.format("%.2f km", totalDistance)
|
||||||
|
SpeedUnit.MPH -> String.format("%.2f mile", totalDistance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CSCData.displayGearRatio(): String {
|
||||||
|
return String.format(Locale.US, "%.1f", gearRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun String.toSpeedUnit(): SpeedUnit {
|
||||||
|
return when (this) {
|
||||||
|
DISPLAY_KM_H -> SpeedUnit.KM_H
|
||||||
|
DISPLAY_M_S -> SpeedUnit.M_S
|
||||||
|
DISPLAY_MPH -> SpeedUnit.MPH
|
||||||
|
else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $this")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SpeedUnit.temperatureSettingsItems(): RadioGroupViewEntity {
|
||||||
|
return RadioGroupViewEntity(
|
||||||
|
SpeedUnit.values().map { createRadioButtonItem(it, this) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRadioButtonItem(unit: SpeedUnit, selectedSpeedUnit: SpeedUnit): RadioButtonItem {
|
||||||
|
return RadioButtonItem(displayTemperature(unit), unit == selectedSpeedUnit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayTemperature(unit: SpeedUnit): String {
|
||||||
|
return when (unit) {
|
||||||
|
SpeedUnit.KM_H -> DISPLAY_KM_H
|
||||||
|
SpeedUnit.M_S -> DISPLAY_M_S
|
||||||
|
SpeedUnit.MPH -> DISPLAY_MPH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,7 +10,13 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
||||||
|
import no.nordicsemi.android.service.*
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.DeviceConnectingView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.DeviceDisconnectedView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.NoDeviceView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.Reason
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CSCScreen() {
|
fun CSCScreen() {
|
||||||
@@ -18,15 +24,22 @@ fun CSCScreen() {
|
|||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.csc_title)) {
|
val navigateUp = { viewModel.onEvent(NavigateUp) }
|
||||||
viewModel.onEvent(OnDisconnectButtonClick)
|
|
||||||
}
|
BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp)
|
||||||
|
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
// when (state) {
|
when (state.cscManagerState) {
|
||||||
// is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) }
|
NoDeviceState -> NoDeviceView()
|
||||||
// LoadingState -> DeviceConnectingView()
|
is WorkingState -> when (state.cscManagerState.result) {
|
||||||
// }.exhaustive
|
is ConnectingResult,
|
||||||
|
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(OnDisconnectButtonClick) }
|
||||||
|
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
|
||||||
|
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
|
||||||
|
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
|
||||||
|
is SuccessResult -> CSCContentView(state.cscManagerState.result.data, state.speedUnit) { viewModel.onEvent(it) }
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
import no.nordicsemi.android.csc.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
|
import no.nordicsemi.android.service.BleManagerResult
|
||||||
|
|
||||||
internal sealed class CSCViewState
|
internal data class CSCViewState(
|
||||||
|
val speedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||||
|
val cscManagerState: CSCMangerState = NoDeviceState
|
||||||
|
)
|
||||||
|
|
||||||
internal object LoadingState : CSCViewState()
|
internal sealed class CSCMangerState
|
||||||
|
|
||||||
internal data class DisplayDataState(val data: CSCData) : CSCViewState()
|
internal data class WorkingState(val result: BleManagerResult<CSCData>) : CSCMangerState()
|
||||||
|
|
||||||
|
internal object NoDeviceState : CSCMangerState()
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent
|
|||||||
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
||||||
|
|
||||||
internal object OnDisconnectButtonClick : CSCViewEvent()
|
internal object OnDisconnectButtonClick : CSCViewEvent()
|
||||||
|
|
||||||
|
internal object NavigateUp : CSCViewEvent()
|
||||||
|
|||||||
@@ -16,22 +16,22 @@ import no.nordicsemi.android.theme.view.ScreenSection
|
|||||||
import no.nordicsemi.android.theme.view.SectionTitle
|
import no.nordicsemi.android.theme.view.SectionTitle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SensorsReadingView(state: CSCData) {
|
internal fun SensorsReadingView(state: CSCData, speedUnit: SpeedUnit) {
|
||||||
ScreenSection {
|
ScreenSection {
|
||||||
SectionTitle(resId = R.drawable.ic_records, title = "Records")
|
SectionTitle(resId = R.drawable.ic_records, title = "Records")
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
KeyValueField(stringResource(id = R.string.csc_field_speed), state.displaySpeed())
|
KeyValueField(stringResource(id = R.string.csc_field_speed), state.displaySpeed(speedUnit))
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.csc_field_cadence), state.displayCadence())
|
KeyValueField(stringResource(id = R.string.csc_field_cadence), state.displayCadence())
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.csc_field_distance), state.displayDistance())
|
KeyValueField(stringResource(id = R.string.csc_field_distance), state.displayDistance(speedUnit))
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(
|
KeyValueField(
|
||||||
stringResource(id = R.string.csc_field_total_distance),
|
stringResource(id = R.string.csc_field_total_distance),
|
||||||
state.displayTotalDistance()
|
state.displayTotalDistance(speedUnit)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), state.displayGearRatio())
|
KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), state.displayGearRatio())
|
||||||
@@ -46,5 +46,5 @@ internal fun SensorsReadingView(state: CSCData) {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
private fun Preview() {
|
||||||
SensorsReadingView(CSCData())
|
SensorsReadingView(CSCData(), SpeedUnit.KM_H)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ package no.nordicsemi.android.csc.viewmodel
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.csc.data.CSCRepository
|
import no.nordicsemi.android.csc.data.CSCRepository
|
||||||
import no.nordicsemi.android.csc.data.DisconnectCommand
|
|
||||||
import no.nordicsemi.android.csc.data.SetWheelSizeCommand
|
|
||||||
import no.nordicsemi.android.csc.repository.CSCService
|
|
||||||
import no.nordicsemi.android.csc.repository.CSC_SERVICE_UUID
|
import no.nordicsemi.android.csc.repository.CSC_SERVICE_UUID
|
||||||
import no.nordicsemi.android.csc.view.*
|
import no.nordicsemi.android.csc.view.*
|
||||||
import no.nordicsemi.android.navigation.*
|
import no.nordicsemi.android.navigation.*
|
||||||
import no.nordicsemi.android.service.BleManagerStatus
|
|
||||||
import no.nordicsemi.android.service.ServiceManager
|
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import no.nordicsemi.android.utils.getDevice
|
import no.nordicsemi.android.utils.getDevice
|
||||||
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
||||||
@@ -21,19 +19,23 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CSCViewModel @Inject constructor(
|
internal class CSCViewModel @Inject constructor(
|
||||||
private val repository: CSCRepository,
|
private val repository: CSCRepository,
|
||||||
private val serviceManager: ServiceManager,
|
|
||||||
private val navigationManager: NavigationManager
|
private val navigationManager: NavigationManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = repository.data.combine(repository.status) { data, status ->
|
private val _state = MutableStateFlow(CSCViewState())
|
||||||
// when (status) {
|
val state = _state.asStateFlow()
|
||||||
// BleManagerStatus.CONNECTING -> LoadingState
|
|
||||||
// BleManagerStatus.OK,
|
|
||||||
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
|
|
||||||
// }
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
if (!repository.isRunning.value) {
|
||||||
|
requestBluetoothDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.data.onEach {
|
||||||
|
_state.value = _state.value.copy(cscManagerState = WorkingState(it))
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestBluetoothDevice() {
|
||||||
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CSC_SERVICE_UUID))
|
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CSC_SERVICE_UUID))
|
||||||
|
|
||||||
navigationManager.recentResult.onEach {
|
navigationManager.recentResult.onEach {
|
||||||
@@ -41,44 +43,30 @@ internal class CSCViewModel @Inject constructor(
|
|||||||
handleArgs(it)
|
handleArgs(it)
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
repository.status.onEach {
|
|
||||||
if (it == BleManagerStatus.DISCONNECTED) {
|
|
||||||
navigationManager.navigateUp()
|
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleArgs(args: DestinationResult) {
|
private fun handleArgs(args: DestinationResult) {
|
||||||
when (args) {
|
when (args) {
|
||||||
is CancelDestinationResult -> navigationManager.navigateUp()
|
is CancelDestinationResult -> navigationManager.navigateUp()
|
||||||
is SuccessDestinationResult -> serviceManager.startService(CSCService::class.java, args.getDevice())
|
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(event: CSCViewEvent) {
|
fun onEvent(event: CSCViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
is OnSelectedSpeedUnitSelected -> setSpeedUnit(event.selectedSpeedUnit)
|
||||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
is OnWheelSizeSelected -> repository.setWheelSize(event.wheelSize)
|
||||||
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
OnDisconnectButtonClick -> disconnect()
|
||||||
|
NavigateUp -> navigationManager.navigateUp()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
private fun setSpeedUnit(speedUnit: SpeedUnit) {
|
||||||
repository.setSpeedUnit(event.selectedSpeedUnit)
|
_state.value = _state.value.copy(speedUnit = speedUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
private fun disconnect() {
|
||||||
repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize))
|
repository.release()
|
||||||
}
|
navigationManager.navigateUp()
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
|
||||||
repository.sendNewServiceCommand(DisconnectCommand)
|
|
||||||
repository.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
repository.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user