Redesign manager approach

This commit is contained in:
Sylwester Zieliński
2022-02-07 14:57:04 +01:00
parent d16a908d6b
commit 7c69fe14a5
54 changed files with 748 additions and 662 deletions

View File

@@ -1,32 +1,68 @@
package no.nordicsemi.android.service package no.nordicsemi.android.service
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 androidx.annotation.IntRange
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.callback.DataReceivedCallback
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
import no.nordicsemi.android.ble.data.Data
import java.util.* import java.util.*
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")
abstract class BatteryManager( abstract class BatteryManager(context: Context, protected val scope: CoroutineScope) : BleManager(context) {
context: Context,
protected val scope: CoroutineScope,
) : BleManager(context) {
private val TAG = "BLE-MANAGER" private val TAG = "BLE-MANAGER"
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private val batteryLevelDataCallback: DataReceivedCallback =
object : BatteryLevelDataCallback() {
override fun onBatteryLevelChanged(
device: BluetoothDevice,
@IntRange(from = 0, to = 100) batteryLevel: Int
) {
onBatteryLevelChanged(batteryLevel)
}
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
log(Log.WARN, "Invalid Battery Level data received: $data")
}
}
protected abstract fun onBatteryLevelChanged(batteryLevel: Int)
fun readBatteryLevelCharacteristic() {
if (isConnected) {
readCharacteristic(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
}
}
fun enableBatteryLevelCharacteristicNotifications() { fun enableBatteryLevelCharacteristicNotifications() {
if (isConnected) { if (isConnected) {
// If the Battery Level characteristic is null, the request will be ignored
setNotificationCallback(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
enableNotifications(batteryLevelCharacteristic)
.done { device: BluetoothDevice? ->
log(Log.INFO, "Battery Level notifications enabled")
}
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
} }
} }
@@ -37,6 +73,7 @@ abstract class BatteryManager(
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() { protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
override fun initialize() { override fun initialize() {
readBatteryLevelCharacteristic()
enableBatteryLevelCharacteristicNotifications() enableBatteryLevelCharacteristicNotifications()
} }
@@ -48,12 +85,13 @@ abstract class BatteryManager(
return batteryLevelCharacteristic != null return batteryLevelCharacteristic != null
} }
override fun onServicesInvalidated() { override fun onDeviceDisconnected() {
batteryLevelCharacteristic = null batteryLevelCharacteristic = null
onBatteryLevelChanged(0)
} }
} }
fun releaseScope() { fun release() {
scope.cancel() scope.cancel()
} }
} }

View File

@@ -45,6 +45,7 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
} }
fun setValue(value: T) { fun setValue(value: T) {
_status.tryEmit(SuccessResult(value)) Log.d("AAATESTAAA", "setValue()")
_status.value = SuccessResult(value)
} }
} }

View File

@@ -1,4 +1,4 @@
package no.nordicsemi.android.theme.view package no.nordicsemi.android.theme.view.scanner
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -19,6 +19,7 @@ import androidx.compose.ui.text.style.TextAlign
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.theme.R import no.nordicsemi.android.theme.R
import no.nordicsemi.android.theme.view.ScreenSection
@Composable @Composable
fun DeviceConnectingView() { fun DeviceConnectingView() {

View File

@@ -0,0 +1,72 @@
package no.nordicsemi.android.theme.view.scanner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HighlightOff
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.R
import no.nordicsemi.android.theme.view.ScreenSection
enum class Reason {
USER, LINK_LOSS, MISSING_SERVICE
}
@Composable
fun DeviceDisconnectedView(reason: Reason) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
ScreenSection {
Icon(
imageVector = Icons.Default.HighlightOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
)
.padding(8.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_disconnected),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.size(16.dp))
val text = when (reason) {
Reason.USER -> stringResource(id = R.string.device_reason_user)
Reason.LINK_LOSS -> stringResource(id = R.string.device_reason_link_loss)
Reason.MISSING_SERVICE -> stringResource(id = R.string.device_reason_missing_service)
}
Text(
text = text,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Preview
@Composable
fun DeviceDisconnectedView_Preview() {
DeviceConnectingView()
}

View File

@@ -0,0 +1,74 @@
package no.nordicsemi.android.theme.view.scanner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HourglassTop
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.R
import no.nordicsemi.android.theme.view.ScreenSection
@Composable
fun NoDeviceView() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
ScreenSection {
Icon(
imageVector = Icons.Default.HourglassTop,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
)
.padding(8.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_connecting),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_explanation),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_please_wait),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
}
}
}
@Preview
@Composable
fun NoDeviceView_Preview() {
DeviceConnectingView()
}

View File

@@ -11,6 +11,11 @@
<string name="disconnect">DISCONNECT</string> <string name="disconnect">DISCONNECT</string>
<string name="field_battery">Battery</string> <string name="field_battery">Battery</string>
<string name="device_disconnected">Disconnected</string>
<string name="device_reason_user">Device disconnected successfully.</string>
<string name="device_reason_link_loss">Device signal has been lost.</string>
<string name="device_reason_missing_service">Device was disconnected, because required services are missing.</string>
<string name="device_connecting">Connecting</string> <string name="device_connecting">Connecting</string>
<string name="device_explanation">The mobile is trying to connect to peripheral device.</string> <string name="device_explanation">The mobile is trying to connect to peripheral device.</string>
<string name="device_please_wait">Please wait...</string> <string name="device_please_wait">Please wait...</string>

View File

@@ -2,9 +2,14 @@ package no.nordicsemi.android.utils
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import no.nordicsemi.android.navigation.ParcelableArgument import no.nordicsemi.android.navigation.ParcelableArgument
import no.nordicsemi.android.navigation.SuccessDestinationResult import no.nordicsemi.android.navigation.SuccessDestinationResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
@@ -38,3 +43,12 @@ fun <T> NavController.consumeResult(value: String): T? {
?.set(value, null) ?.set(value, null)
} }
} }
private val exceptionHandler = CoroutineExceptionHandler { _, t ->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
}
fun CoroutineScope.launchWithCatch(block: suspend CoroutineScope.() -> Unit) =
launch(Job() + exceptionHandler) {
block()
}

View File

@@ -1,73 +1,50 @@
package no.nordicsemi.android.bps.data package no.nordicsemi.android.bps.data
import kotlinx.coroutines.flow.MutableStateFlow import android.bluetooth.BluetoothDevice
import kotlinx.coroutines.flow.StateFlow import android.content.Context
import kotlinx.coroutines.flow.asStateFlow import android.util.Log
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.service.BleManagerStatus import dagger.hilt.android.scopes.ViewModelScoped
import java.util.* import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.bps.repository.BPSManager
import no.nordicsemi.android.service.BleManagerResult
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton @ViewModelScoped
internal class BPSRepository @Inject constructor() { internal class BPSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val _data = MutableStateFlow(BPSData()) fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
val data: StateFlow<BPSData> = _data val scope = this
val manager = BPSManager(context, scope)
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) manager.dataHolder.status.onEach {
val status = _status.asStateFlow() trySend(it)
}.launchIn(scope)
fun setIntermediateCuffPressure( try {
cuffPressure: Float, manager.connect(device)
unit: Int, .useAutoConnect(false)
pulseRate: Float?, .retry(3, 100)
userID: Int?, .suspend()
status: BloodPressureTypes.BPMStatus?, } catch (e: Exception) {
calendar: Calendar? e.printStackTrace()
) {
_data.tryEmit(_data.value.copy(
cuffPressure = cuffPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
calendar = calendar
))
} }
fun setBloodPressureMeasurement( awaitClose {
systolic: Float, launch {
diastolic: Float, manager.disconnect().suspend()
meanArterialPressure: Float,
unit: Int,
pulseRate: Float?,
userID: Int?,
status: BloodPressureTypes.BPMStatus?,
calendar: Calendar?
) {
_data.tryEmit(_data.value.copy(
systolic = systolic,
diastolic = diastolic,
meanArterialPressure = meanArterialPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
calendar = calendar
))
} }
fun setBatteryLevel(batteryLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
} }
fun clear() {
_status.value = BleManagerStatus.CONNECTING
_data.tryEmit(BPSData())
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
} }
} }

View File

@@ -0,0 +1,31 @@
package no.nordicsemi.android.bps.data
import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse
import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse
internal fun BPSData.copyWithNewResponse(response: IntermediateCuffPressureResponse): BPSData {
return with (response) {
copy(
cuffPressure = cuffPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
calendar = timestamp
)
}
}
internal fun BPSData.copyWithNewResponse(response: BloodPressureMeasurementResponse): BPSData {
return with (response) {
copy(
systolic = systolic,
diastolic = diastolic,
meanArterialPressure = meanArterialPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
)
}
}

View File

@@ -26,109 +26,104 @@ 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 dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementResponse
import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.data.BPSRepository import no.nordicsemi.android.bps.data.copyWithNewResponse
import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.service.CloseableCoroutineScope
import java.util.* import java.util.*
import javax.inject.Inject
val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb")
private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb")
private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb") private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb")
@ViewModelScoped private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
internal class BPSManager @Inject constructor( private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
@ApplicationContext context: Context,
private val dataHolder: BPSRepository
) : BatteryManager(context, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)) {
internal class BPSManager(
@ApplicationContext context: Context,
private val scope: CoroutineScope
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var bpmCharacteristic: BluetoothGattCharacteristic? = null private var bpmCharacteristic: BluetoothGattCharacteristic? = null
private var icpCharacteristic: BluetoothGattCharacteristic? = null private var icpCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t-> private val data = MutableStateFlow(BPSData())
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) val dataHolder = ConnectionObserverAdapter<BPSData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
} }
override fun onBatteryLevelChanged(batteryLevel: Int) { override fun getMinLogPriority(): Int {
dataHolder.setBatteryLevel(batteryLevel) return Log.VERBOSE
} }
private inner class BloodPressureManagerGattCallback : BatteryManagerGattCallback() { override fun log(priority: Int, message: String) {
Log.println(priority, "AAA", message)
}
override fun getGattCallback(): BleManagerGattCallback {
return BloodPressureManagerGattCallback()
}
private inner class BloodPressureManagerGattCallback : BleManagerGattCallback() {
@OptIn(ExperimentalCoroutinesApi::class)
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
setNotificationCallback(icpCharacteristic).asValidResponseFlow<IntermediateCuffPressureResponse>() setNotificationCallback(icpCharacteristic).asValidResponseFlow<IntermediateCuffPressureResponse>()
.onEach { .onEach { data.tryEmit(data.value.copyWithNewResponse(it)) }
dataHolder.setIntermediateCuffPressure( .launchIn(scope)
it.cuffPressure,
it.unit,
it.pulseRate,
it.userID,
it.status,
it.timestamp
)
}.launchIn(scope)
setIndicationCallback(bpmCharacteristic).asValidResponseFlow<BloodPressureMeasurementResponse>() setIndicationCallback(bpmCharacteristic).asValidResponseFlow<BloodPressureMeasurementResponse>()
.onEach { data.tryEmit(data.value.copyWithNewResponse(it)) }
.launchIn(scope)
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach { .onEach {
dataHolder.setBloodPressureMeasurement( data.value = data.value.copy(batteryLevel = it.batteryLevel)
it.systolic,
it.diastolic,
it.meanArterialPressure,
it.unit,
it.pulseRate,
it.userID,
it.status,
it.timestamp
)
}.launchIn(scope) }.launchIn(scope)
scope.launch(exceptionHandler) { enableNotifications(icpCharacteristic).enqueue()
enableNotifications(icpCharacteristic).suspend() enableIndications(bpmCharacteristic).enqueue()
} enableNotifications(batteryLevelCharacteristic).enqueue()
scope.launch(exceptionHandler) {
enableIndications(bpmCharacteristic).suspend()
}
} }
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(BPS_SERVICE_UUID) gatt.getService(BPS_SERVICE_UUID)?.run {
if (service != null) { bpmCharacteristic = getCharacteristic(BPM_CHARACTERISTIC_UUID)
bpmCharacteristic = service.getCharacteristic(BPM_CHARACTERISTIC_UUID) icpCharacteristic = getCharacteristic(ICP_CHARACTERISTIC_UUID)
icpCharacteristic = service.getCharacteristic(ICP_CHARACTERISTIC_UUID)
} }
return bpmCharacteristic != null && icpCharacteristic != null gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return bpmCharacteristic != null && batteryLevelCharacteristic != null
} }
override fun onServicesInvalidated() {}
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
super.isOptionalServiceSupported(gatt) // ignore the result of this super.isOptionalServiceSupported(gatt) // ignore the result of this
return icpCharacteristic != null return icpCharacteristic != null
} }
override fun onDeviceDisconnected() { override fun onServicesInvalidated() {
icpCharacteristic = null icpCharacteristic = null
bpmCharacteristic = null bpmCharacteristic = null
batteryLevelCharacteristic = null
} }
} }
override fun getGattCallback(): BleManagerGattCallback {
return BloodPressureManagerGattCallback()
}
} }

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.bps.view package no.nordicsemi.android.bps.view
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -10,8 +11,12 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.bps.R import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.viewmodel.BPSViewModel import no.nordicsemi.android.bps.viewmodel.BPSViewModel
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.DeviceConnectingView 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 import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
@@ -24,10 +29,19 @@ fun BPSScreen() {
viewModel.onEvent(DisconnectEvent) viewModel.onEvent(DisconnectEvent)
} }
Log.d("AAATESTAAA", "state: $state")
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
is DisplayDataState -> BPSContentView(state.data) { viewModel.onEvent(it) } NoDeviceState -> NoDeviceView()
LoadingState -> DeviceConnectingView() is WorkingState -> when (state.result) {
is ConnectingResult -> DeviceConnectingView()
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
is ReadyResult -> DeviceConnectingView()
is SuccessResult -> BPSContentView(state.result.data) { viewModel.onEvent(it) }
}
}.exhaustive }.exhaustive
} }
} }

View File

@@ -1,9 +0,0 @@
package no.nordicsemi.android.bps.view
import no.nordicsemi.android.bps.data.BPSData
internal sealed class BPSViewState
internal object LoadingState : BPSViewState()
internal data class DisplayDataState(val data: BPSData) : BPSViewState()

View File

@@ -0,0 +1,9 @@
package no.nordicsemi.android.bps.view
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState
internal data class WorkingState(val result: BleManagerResult<BPSData>) : BPSViewState()
internal object NoDeviceState : BPSViewState()

View File

@@ -1,20 +1,16 @@
package no.nordicsemi.android.bps.viewmodel package no.nordicsemi.android.bps.viewmodel
import android.bluetooth.BluetoothDevice
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.bps.data.BPSRepository import no.nordicsemi.android.bps.data.BPSRepository
import no.nordicsemi.android.bps.repository.BPSManager
import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID import no.nordicsemi.android.bps.repository.BPS_SERVICE_UUID
import no.nordicsemi.android.bps.view.BPSScreenViewEvent import no.nordicsemi.android.bps.view.*
import no.nordicsemi.android.bps.view.DisconnectEvent
import no.nordicsemi.android.bps.view.DisplayDataState
import no.nordicsemi.android.bps.view.LoadingState
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ConnectionObserverAdapter
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.DiscoveredBluetoothDevice
@@ -23,18 +19,12 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class BPSViewModel @Inject constructor( internal class BPSViewModel @Inject constructor(
private val bpsManager: BPSManager,
private val repository: BPSRepository, private val repository: BPSRepository,
private val navigationManager: NavigationManager private val navigationManager: NavigationManager
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
when (status) { val state = _state.asStateFlow()
BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(BPS_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(BPS_SERVICE_UUID))
@@ -44,29 +34,6 @@ internal class BPSViewModel @Inject constructor(
handleArgs(it) handleArgs(it)
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
bpsManager.setConnectionObserver(object : ConnectionObserverAdapter() {
override fun onDeviceConnected(device: BluetoothDevice) {
super.onDeviceConnected(device)
repository.setNewStatus(BleManagerStatus.OK)
}
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
super.onDeviceFailedToConnect(device, reason)
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
super.onDeviceDisconnected(device, reason)
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
})
repository.status.onEach {
if (it == BleManagerStatus.DISCONNECTED) {
navigationManager.navigateUp()
}
}.launchIn(viewModelScope)
} }
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
@@ -78,28 +45,13 @@ internal class BPSViewModel @Inject constructor(
fun onEvent(event: BPSScreenViewEvent) { fun onEvent(event: BPSScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> onDisconnectButtonClick() DisconnectEvent -> navigationManager.navigateUp()
}.exhaustive }.exhaustive
} }
private fun connectDevice(device: DiscoveredBluetoothDevice) { private fun connectDevice(device: DiscoveredBluetoothDevice) {
bpsManager.connect(device.device) repository.downloadData(device.device).onEach {
.useAutoConnect(false) _state.value = WorkingState(it)
.retry(3, 100) }.launchIn(viewModelScope)
.enqueue()
}
private fun onDisconnectButtonClick() {
if (bpsManager.isConnected) {
bpsManager.disconnect().enqueue()
} else {
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
}
override fun onCleared() {
super.onCleared()
repository.clear()
bpsManager.release()
} }
} }

View File

@@ -10,7 +10,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import no.nordicsemi.android.bps.data.BPSRepository
import no.nordicsemi.android.bps.repository.BPSManager import no.nordicsemi.android.bps.repository.BPSManager
import no.nordicsemi.android.bps.view.DisconnectEvent import no.nordicsemi.android.bps.view.DisconnectEvent
import no.nordicsemi.android.bps.viewmodel.BPSViewModel import no.nordicsemi.android.bps.viewmodel.BPSViewModel

View File

@@ -5,7 +5,6 @@ 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.cgms.data.CGMServiceCommand import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ForegroundBleService import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@@ -21,11 +20,11 @@ internal class CGMService : ForegroundBleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach { // status.onEach {
val status = it.mapToSimpleManagerStatus() // val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status) // repository.setNewStatus(status)
stopIfDisconnected(status) // stopIfDisconnected(status)
}.launchIn(scope) // }.launchIn(scope)
repository.command.onEach { repository.command.onEach {
when (it) { when (it) {

View File

@@ -11,8 +11,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.cgms.R import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun CGMScreen() { fun CGMScreen() {
@@ -25,10 +23,10 @@ fun CGMScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -25,11 +25,11 @@ internal class CGMScreenViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -1,6 +1,5 @@
package no.nordicsemi.android.csc.data package no.nordicsemi.android.csc.data
import dagger.hilt.android.scopes.ServiceScoped
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import no.nordicsemi.android.csc.view.SpeedUnit import no.nordicsemi.android.csc.view.SpeedUnit
@@ -8,7 +7,7 @@ import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ServiceScoped @Singleton
internal class CSCRepository @Inject constructor() { internal class CSCRepository @Inject constructor() {
private val _data = MutableStateFlow(CSCData()) private val _data = MutableStateFlow(CSCData())

View File

@@ -30,13 +30,10 @@ 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.cancel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementResponse import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow import no.nordicsemi.android.ble.ktx.asValidResponseFlow
@@ -58,7 +55,7 @@ internal class CSCRepo @Inject constructor(
private val context: Context, private val context: Context,
) { ) {
suspend fun downloadData(device: BluetoothDevice) = callbackFlow<BleManagerResult<CSCData>> { suspend fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<CSCData>> = callbackFlow {
val scope = CoroutineScope(coroutineContext) val scope = CoroutineScope(coroutineContext)
val manager = CSCManager(context, scope) val manager = CSCManager(context, scope)
@@ -84,9 +81,8 @@ internal class CSCRepo @Inject constructor(
internal class CSCManager( internal class CSCManager(
context: Context, context: Context,
scope: CoroutineScope, private val scope: CoroutineScope,
// private val channel: SendChannel<BleManagerResult<CSCData>> ) : BleManager(context) {
) : BatteryManager(context, scope) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
@@ -97,7 +93,7 @@ internal class CSCManager(
private val data = MutableStateFlow(CSCData()) private val data = MutableStateFlow(CSCData())
val dataHolder = ConnectionObserverAdapter<CSCData>() val dataHolder = ConnectionObserverAdapter<CSCData>()
private val exceptionHandler = CoroutineExceptionHandler { context, t -> private val exceptionHandler = CoroutineExceptionHandler { _, t ->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
} }
@@ -109,7 +105,7 @@ internal class CSCManager(
}.launchIn(scope) }.launchIn(scope)
} }
override fun getGattCallback(): BatteryManagerGattCallback { override fun getGattCallback(): BleManagerGattCallback {
return CSCManagerGattCallback() return CSCManagerGattCallback()
} }
@@ -117,7 +113,7 @@ internal class CSCManager(
wheelSize = value wheelSize = value
} }
private inner class CSCManagerGattCallback : BatteryManagerGattCallback() { private inner class CSCManagerGattCallback : BleManagerGattCallback() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
@@ -128,14 +124,17 @@ internal class CSCManager(
val totalDistance = it.getTotalDistance(wheelSize.value.toFloat()) val totalDistance = it.getTotalDistance(wheelSize.value.toFloat())
val distance = it.getDistance(wheelCircumference, previousResponse) val distance = it.getDistance(wheelCircumference, previousResponse)
val speed = it.getSpeed(wheelCircumference, previousResponse) val speed = it.getSpeed(wheelCircumference, previousResponse)
//todo
data.value.copy(totalDistance, )
repository.setNewDistance(totalDistance, distance, speed, wheelSize)
val crankCadence = it.getCrankCadence(previousResponse) val crankCadence = it.getCrankCadence(previousResponse)
val gearRatio = it.getGearRatio(previousResponse) val gearRatio = it.getGearRatio(previousResponse)
repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize)
data.tryEmit(data.value.copy(
totalDistance = totalDistance,
distance = distance,
speed = speed,
wheelSize = wheelSize,
cadence = crankCadence,
gearRatio = gearRatio,
))
} }
previousResponse = it previousResponse = it
@@ -155,16 +154,16 @@ internal class CSCManager(
} }
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(CSC_SERVICE_UUID) gatt.getService(CSC_SERVICE_UUID)?.run {
if (service != null) { cscMeasurementCharacteristic = getCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID)
cscMeasurementCharacteristic = service.getCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID)
batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
} }
return cscMeasurementCharacteristic != null gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return cscMeasurementCharacteristic != null && batteryLevelCharacteristic != null
} }
override fun onServicesInvalidated() { override fun onServicesInvalidated() {
super.onServicesInvalidated()
cscMeasurementCharacteristic = null cscMeasurementCharacteristic = null
batteryLevelCharacteristic = null batteryLevelCharacteristic = null
} }

View File

@@ -16,17 +16,11 @@ internal class CSCService : ForegroundBleService() {
@Inject @Inject
lateinit var repository: CSCRepository lateinit var repository: CSCRepository
override val manager: CSCManager by lazy { CSCManager(this, scope, repository) } override val manager: CSCManager by lazy { CSCManager(this, scope) }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach { repository.command.onEach {
when (it) { when (it) {
DisconnectCommand -> stopSelf() DisconnectCommand -> stopSelf()

View File

@@ -11,8 +11,6 @@ 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.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun CSCScreen() { fun CSCScreen() {
@@ -25,10 +23,10 @@ fun CSCScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -26,11 +26,11 @@ internal class CSCViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -6,8 +6,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.dfu.R import no.nordicsemi.dfu.R
import no.nordicsemi.dfu.viewmodel.DFUViewModel import no.nordicsemi.dfu.viewmodel.DFUViewModel
@@ -21,9 +19,9 @@ fun DFUScreen() {
viewModel.onEvent(OnDisconnectButtonClick) viewModel.onEvent(OnDisconnectButtonClick)
} }
when (state) { // when (state) {
is DisplayDataState -> DFUContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> DFUContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }

View File

@@ -37,11 +37,11 @@ internal class DFUViewModel @Inject constructor(
?.run { createInstallingStateWithNewStatus(state, status) } ?.run { createInstallingStateWithNewStatus(state, status) }
?: state ?: state
}.combine(repository.status) { data, status -> }.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -0,0 +1,43 @@
package no.nordicsemi.android.gls.data
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse
internal fun GlucoseMeasurementResponse.toRecord(): GLSRecord {
return this.let {
GLSRecord(
sequenceNumber = it.sequenceNumber,
time = it.time,
glucoseConcentration = it.glucoseConcentration ?: 0f,
unit = it.unit?.let { ConcentrationUnit.create(it) }
?: ConcentrationUnit.UNIT_KGPL,
type = RecordType.createOrNull(it.type),
sampleLocation = SampleLocation.createOrNull(it.sampleLocation),
status = it.status
)
}
}
internal fun GlucoseMeasurementContextResponse.toMeasurementContext(): MeasurementContext {
return this.let {
MeasurementContext(
sequenceNumber = it.sequenceNumber,
carbohydrate = it.carbohydrate,
carbohydrateAmount = it.carbohydrateAmount ?: 0f,
meal = it.meal,
tester = it.tester,
health = it.health,
exerciseDuration = it.exerciseDuration ?: 0,
exerciseIntensity = it.exerciseIntensity ?: 0,
medication = it.medication,
medicationQuantity = it.medicationAmount ?: 0f,
medicationUnit = it.medicationUnit?.let { MedicationUnit.create(it) }
?: MedicationUnit.UNIT_KG,
HbA1c = it.hbA1c ?: 0f
)
}
}
internal fun GLSRecord.copyWithNewContext(response: GlucoseMeasurementContextResponse): GLSRecord {
return copy(context = context)
}

View File

@@ -19,29 +19,28 @@
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * 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. * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package no.nordicsemi.android.gls.repository package no.nordicsemi.android.gls.data
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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextResponse
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementResponse
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
import no.nordicsemi.android.ble.ktx.asValidResponseFlow 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.gls.data.* import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.utils.launchWithCatch
import no.nordicsemi.android.service.CloseableCoroutineScope
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@@ -52,75 +51,58 @@ private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000
private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-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 GLSManager @Inject constructor( internal class GLSManager @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext
private val repository: GLSRepository context: Context,
) : BatteryManager(context) { private val scope: CoroutineScope
) : BleManager(context) {
private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t-> private val data = MutableStateFlow(GLSData())
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) val dataHolder = ConnectionObserverAdapter<GLSData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
} }
override fun onBatteryLevelChanged(batteryLevel: Int) { override fun getGattCallback(): BleManagerGattCallback {
repository.setNewBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return GlucoseManagerGattCallback() return GlucoseManagerGattCallback()
} }
private inner class GlucoseManagerGattCallback : BatteryManagerGattCallback() { private inner class GlucoseManagerGattCallback : BleManagerGattCallback() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow<GlucoseMeasurementResponse>() setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow<GlucoseMeasurementResponse>()
.onEach { .onEach { data.tryEmit(data.value.copy(records = data.value.records + it.toRecord())) }
val record = GLSRecord( .launchIn(scope)
sequenceNumber = it.sequenceNumber,
time = it.time,
glucoseConcentration = it.glucoseConcentration ?: 0f,
unit = it.unit?.let { ConcentrationUnit.create(it) }
?: ConcentrationUnit.UNIT_KGPL,
type = RecordType.createOrNull(it.type),
sampleLocation = SampleLocation.createOrNull(it.sampleLocation),
status = it.status
)
repository.addNewRecord(record)
}.launchIn(scope)
setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow<GlucoseMeasurementContextResponse>() setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow<GlucoseMeasurementContextResponse>()
.onEach { .onEach {
val context = MeasurementContext( val context = it.toMeasurementContext()
sequenceNumber = it.sequenceNumber, data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let {
carbohydrate = it.carbohydrate, it.context = context
carbohydrateAmount = it.carbohydrateAmount ?: 0f, }
meal = it.meal, data.tryEmit(data.value)
tester = it.tester,
health = it.health,
exerciseDuration = it.exerciseDuration ?: 0,
exerciseIntensity = it.exerciseIntensity ?: 0,
medication = it.medication,
medicationQuantity = it.medicationAmount ?: 0f,
medicationUnit = it.medicationUnit?.let { MedicationUnit.create(it) }
?: MedicationUnit.UNIT_KG,
HbA1c = it.hbA1c ?: 0f
)
repository.addNewContext(context)
}.launchIn(scope) }.launchIn(scope)
setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow<RecordAccessControlPointResponse>() setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow<RecordAccessControlPointResponse>()
.onEach { .onEach {
if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) { if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) {
onNumberOfRecordsReceived(it) onNumberOfRecordsReceived(it)
} else if(it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) { } else if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) {
onRecordAccessOperationCompletedWithNoRecordsFound(it) onRecordAccessOperationCompletedWithNoRecordsFound(it)
} else if (it.isOperationCompleted && it.wereRecordsFound()) { } else if (it.isOperationCompleted && it.wereRecordsFound()) {
onRecordAccessOperationCompleted(it) onRecordAccessOperationCompleted(it)
@@ -129,15 +111,15 @@ internal class GLSManager @Inject constructor(
} }
}.launchIn(scope) }.launchIn(scope)
scope.launch(exceptionHandler) { setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
enableNotifications(glucoseMeasurementCharacteristic).suspend() .onEach {
} data.value = data.value.copy(batteryLevel = it.batteryLevel)
scope.launch(exceptionHandler) { }.launchIn(scope)
enableNotifications(glucoseMeasurementContextCharacteristic).suspend()
} enableNotifications(glucoseMeasurementCharacteristic).enqueue()
scope.launch(exceptionHandler) { enableNotifications(glucoseMeasurementContextCharacteristic).enqueue()
enableIndications(recordAccessControlPointCharacteristic).suspend() enableIndications(recordAccessControlPointCharacteristic).enqueue()
} enableNotifications(batteryLevelCharacteristic).enqueue()
} }
private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) {
@@ -145,21 +127,23 @@ internal class GLSManager @Inject constructor(
RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS else -> RequestStatus.SUCCESS
} }
repository.setRequestStatus(status) data.tryEmit(data.value.copy(requestStatus = status))
} }
private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) {
repository.setRequestStatus(RequestStatus.SUCCESS) data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
} }
private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) { private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) {
if (response.numberOfRecords > 0) { if (response.numberOfRecords > 0) {
if (repository.records().isNotEmpty()) { if (data.value.records.isNotEmpty()) {
val sequenceNumber = repository.records() val sequenceNumber = data.value.records
.last().sequenceNumber + 1 //TODO check if correct .last().sequenceNumber + 1
writeCharacteristic( writeCharacteristic(
recordAccessControlPointCharacteristic, recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber), RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
sequenceNumber
),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend() ).suspend()
} else { } else {
@@ -170,26 +154,28 @@ internal class GLSManager @Inject constructor(
).suspend() ).suspend()
} }
} }
repository.setRequestStatus(RequestStatus.SUCCESS) data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
} }
private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) { private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) {
log(Log.WARN, "Record Access operation failed (error ${response.errorCode})") log(Log.WARN, "Record Access operation failed (error ${response.errorCode})")
if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { if (response.errorCode == RecordAccessControlPointDataCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
repository.setRequestStatus(RequestStatus.NOT_SUPPORTED) data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
} else { } else {
repository.setRequestStatus(RequestStatus.FAILED) data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
} }
} }
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(GLS_SERVICE_UUID) gatt.getService(GLS_SERVICE_UUID)?.run {
if (service != null) { glucoseMeasurementCharacteristic = getCharacteristic(GM_CHARACTERISTIC)
glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC) glucoseMeasurementContextCharacteristic = getCharacteristic(GM_CONTEXT_CHARACTERISTIC)
glucoseMeasurementContextCharacteristic = service.getCharacteristic(GM_CONTEXT_CHARACTERISTIC) recordAccessControlPointCharacteristic = getCharacteristic(RACP_CHARACTERISTIC)
recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_CHARACTERISTIC)
} }
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null && glucoseMeasurementContextCharacteristic != null gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null && glucoseMeasurementContextCharacteristic != null && batteryLevelCharacteristic != null
} }
override fun onServicesInvalidated() {} override fun onServicesInvalidated() {}
@@ -207,10 +193,10 @@ internal class GLSManager @Inject constructor(
} }
private fun clear() { private fun clear() {
repository.clearRecords() data.tryEmit(data.value.copy(records = emptyList()))
val target = bluetoothDevice val target = bluetoothDevice
if (target != null) { if (target != null) {
repository.setRequestStatus(RequestStatus.SUCCESS) data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
} }
} }
@@ -218,8 +204,8 @@ internal class GLSManager @Inject constructor(
if (recordAccessControlPointCharacteristic == null) return if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return val target = bluetoothDevice ?: return
clear() clear()
repository.setRequestStatus(RequestStatus.PENDING) data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launch(exceptionHandler) { scope.launchWithCatch {
writeCharacteristic( writeCharacteristic(
recordAccessControlPointCharacteristic, recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportLastStoredRecord(), RecordAccessControlPointData.reportLastStoredRecord(),
@@ -231,8 +217,8 @@ internal class GLSManager @Inject constructor(
fun requestFirstRecord() { fun requestFirstRecord() {
if (recordAccessControlPointCharacteristic == null) return if (recordAccessControlPointCharacteristic == null) return
clear() clear()
repository.setRequestStatus(RequestStatus.PENDING) data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launch(exceptionHandler) { scope.launchWithCatch {
writeCharacteristic( writeCharacteristic(
recordAccessControlPointCharacteristic, recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportFirstStoredRecord(), RecordAccessControlPointData.reportFirstStoredRecord(),
@@ -244,8 +230,8 @@ internal class GLSManager @Inject constructor(
fun requestAllRecords() { fun requestAllRecords() {
if (recordAccessControlPointCharacteristic == null) return if (recordAccessControlPointCharacteristic == null) return
clear() clear()
repository.setRequestStatus(RequestStatus.PENDING) data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launch(exceptionHandler) { scope.launchWithCatch {
writeCharacteristic( writeCharacteristic(
recordAccessControlPointCharacteristic, recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportNumberOfAllStoredRecords(), RecordAccessControlPointData.reportNumberOfAllStoredRecords(),
@@ -253,8 +239,4 @@ internal class GLSManager @Inject constructor(
).suspend() ).suspend()
} }
} }
fun release() {
scope.close()
}
} }

View File

@@ -1,55 +1,59 @@
package no.nordicsemi.android.gls.data package no.nordicsemi.android.gls.data
import kotlinx.coroutines.flow.MutableStateFlow import android.bluetooth.BluetoothDevice
import kotlinx.coroutines.flow.StateFlow import android.content.Context
import kotlinx.coroutines.flow.asStateFlow import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.service.BleManagerStatus import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton @ViewModelScoped
internal class GLSRepository @Inject constructor() { internal class GLSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val _data = MutableStateFlow(GLSData()) private var manager: GLSManager? = null
val data: StateFlow<GLSData> = _data.asStateFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<GLSData>> = callbackFlow {
val status = _status.asStateFlow() val scope = this
val managerInstance = manager ?: GLSManager(context, scope).apply {
fun addNewRecord(record: GLSRecord) { try {
val newRecords = _data.value.records.toMutableList().apply { connect(device)
add(record) .useAutoConnect(false)
.retry(3, 100)
.suspend()
} catch (e: Exception) {
e.printStackTrace()
}
}
manager = managerInstance
managerInstance.dataHolder.status.onEach {
trySend(it)
}.launchIn(scope)
awaitClose {
launch {
manager?.disconnect()?.suspend()
manager = null
}
} }
_data.tryEmit(_data.value.copy(records = newRecords))
} }
fun addNewContext(context: MeasurementContext) { fun requestMode(workingMode: WorkingMode) {
_data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let { when (workingMode) {
it.context = context WorkingMode.ALL -> manager?.requestAllRecords()
} WorkingMode.LAST -> manager?.requestLastRecord()
_data.tryEmit(_data.value) WorkingMode.FIRST -> manager?.requestFirstRecord()
} }.exhaustive
fun setRequestStatus(requestStatus: RequestStatus) {
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
}
fun records() = _data.value.records
fun clearRecords() {
_data.tryEmit(_data.value.copy(records = emptyList()))
}
fun setNewBatteryLevel(batteryLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
fun clear() {
_status.value = BleManagerStatus.CONNECTING
_data.tryEmit(GLSData())
} }
} }

View File

@@ -1,9 +0,0 @@
package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData
internal sealed class GLSViewState
internal object LoadingState : GLSViewState()
internal data class DisplayDataState(val data: GLSData) : GLSViewState()

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.gls.main.view package no.nordicsemi.android.gls.main.view
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -10,8 +11,12 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
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.DeviceConnectingView 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 import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
@@ -24,10 +29,19 @@ fun GLSScreen() {
viewModel.onEvent(DisconnectEvent) viewModel.onEvent(DisconnectEvent)
} }
Log.d("AAATESTAAA", "state: $state")
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
is DisplayDataState -> GLSContentView(state.data) { viewModel.onEvent(it) } NoDeviceState -> NoDeviceView()
LoadingState -> DeviceConnectingView() is WorkingState -> when (state.result) {
is ConnectingResult -> DeviceConnectingView()
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
is ReadyResult -> DeviceConnectingView()
is SuccessResult -> GLSContentView(state.result.data) { viewModel.onEvent(it) }
}
}.exhaustive }.exhaustive
} }
} }

View File

@@ -0,0 +1,9 @@
package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState
internal data class WorkingState(val result: BleManagerResult<GLSData>) : BPSViewState()
internal object NoDeviceState : BPSViewState()

View File

@@ -1,19 +1,14 @@
package no.nordicsemi.android.gls.main.viewmodel package no.nordicsemi.android.gls.main.viewmodel
import android.bluetooth.BluetoothDevice
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.*
import no.nordicsemi.android.gls.GlsDetailsDestinationId import no.nordicsemi.android.gls.GlsDetailsDestinationId
import no.nordicsemi.android.gls.data.GLSRepository import no.nordicsemi.android.gls.data.GLSRepository
import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.data.GLS_SERVICE_UUID
import no.nordicsemi.android.gls.main.view.* import no.nordicsemi.android.gls.main.view.*
import no.nordicsemi.android.gls.repository.GLSManager
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ConnectionObserverAdapter
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.DiscoveredBluetoothDevice
@@ -22,18 +17,12 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class GLSViewModel @Inject constructor( internal class GLSViewModel @Inject constructor(
private val glsManager: GLSManager,
private val repository: GLSRepository, private val repository: GLSRepository,
private val navigationManager: NavigationManager private val navigationManager: NavigationManager
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
when (status) { val state = _state.asStateFlow()
BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(GLS_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(GLS_SERVICE_UUID))
@@ -43,29 +32,6 @@ internal class GLSViewModel @Inject constructor(
handleArgs(it) handleArgs(it)
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
glsManager.setConnectionObserver(object : ConnectionObserverAdapter() {
override fun onDeviceConnected(device: BluetoothDevice) {
super.onDeviceConnected(device)
repository.setNewStatus(BleManagerStatus.OK)
}
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
super.onDeviceFailedToConnect(device, reason)
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
super.onDeviceDisconnected(device, reason)
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
})
repository.status.onEach {
if (it == BleManagerStatus.DISCONNECTED) {
navigationManager.navigateUp()
}
}.launchIn(viewModelScope)
} }
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
@@ -77,38 +43,16 @@ internal class GLSViewModel @Inject constructor(
fun onEvent(event: GLSScreenViewEvent) { fun onEvent(event: GLSScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> disconnect() DisconnectEvent -> navigationManager.navigateUp()
is OnWorkingModeSelected -> requestData(event.workingMode) is OnWorkingModeSelected -> repository.requestMode(event.workingMode)
is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, AnyArgument(event.record)) is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, AnyArgument(event.record))
DisconnectEvent -> navigationManager.navigateUp()
}.exhaustive }.exhaustive
} }
private fun connectDevice(device: DiscoveredBluetoothDevice) { private fun connectDevice(device: DiscoveredBluetoothDevice) {
glsManager.connect(device.device) repository.downloadData(device.device).onEach {
.useAutoConnect(false) _state.value = WorkingState(it)
.retry(3, 100) }.launchIn(viewModelScope)
.enqueue()
}
private fun requestData(mode: WorkingMode) {
when (mode) {
WorkingMode.ALL -> glsManager.requestAllRecords()
WorkingMode.LAST -> glsManager.requestLastRecord()
WorkingMode.FIRST -> glsManager.requestFirstRecord()
}.exhaustive
}
private fun disconnect() {
if (glsManager.isConnected) {
glsManager.disconnect().enqueue()
} else {
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
}
}
override fun onCleared() {
super.onCleared()
repository.clear()
glsManager.release()
} }
} }

View File

@@ -9,6 +9,7 @@ dependencies {
implementation libs.chart implementation libs.chart
implementation libs.nordic.ble.common implementation libs.nordic.ble.common
implementation libs.nordic.ble.ktx
implementation libs.nordic.navigation implementation libs.nordic.navigation
implementation libs.nordic.ui.scanner implementation libs.nordic.ui.scanner

View File

@@ -21,15 +21,18 @@
*/ */
package no.nordicsemi.android.hrs.service package no.nordicsemi.android.hrs.service
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 kotlinx.coroutines.CoroutineScope
import androidx.annotation.IntRange import kotlinx.coroutines.flow.launchIn
import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation 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.suspend
import no.nordicsemi.android.ble.ktx.suspendForValidResponse
import no.nordicsemi.android.hrs.data.HRSRepository import no.nordicsemi.android.hrs.data.HRSRepository
import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.service.BatteryManager
import java.util.* import java.util.*
@@ -38,40 +41,15 @@ val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-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 HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
/** internal class HRSManager(
* HRSManager class performs BluetoothGatt operations for connection, service discovery, context: Context,
* enabling notification and reading characteristics. scope: CoroutineScope,
* All operations required to connect to device with BLE Heart Rate Service and reading private val dataHolder: HRSRepository
* heart rate values are performed here. ) : BatteryManager(context, scope) {
*/
internal class HRSManager(context: Context, private val dataHolder: HRSRepository) : BatteryManager(context) {
private var heartRateCharacteristic: BluetoothGattCharacteristic? = null private var heartRateCharacteristic: BluetoothGattCharacteristic? = null
private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null
private val bodySensorLocationDataCallback = object : BodySensorLocationDataCallback() {
override fun onBodySensorLocationReceived(
device: BluetoothDevice,
@BodySensorLocation sensorLocation: Int
) {
dataHolder.setSensorLocation(sensorLocation)
}
}
private val heartRateMeasurementDataCallback = object : HeartRateMeasurementDataCallback() {
override fun onHeartRateMeasurementReceived(
device: BluetoothDevice,
@IntRange(from = 0) heartRate: Int,
contactDetected: Boolean?,
@IntRange(from = 0) energyExpanded: Int?,
rrIntervals: List<Int>?
) {
dataHolder.addNewHeartRate(heartRate)
}
}
override fun onBatteryLevelChanged(batteryLevel: Int) { override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel) dataHolder.setBatteryLevel(batteryLevel)
} }
@@ -80,23 +58,25 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
return HeartRateManagerCallback() return HeartRateManagerCallback()
} }
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving notification, etc.
*/
private inner class HeartRateManagerCallback : BatteryManagerGattCallback() { private inner class HeartRateManagerCallback : BatteryManagerGattCallback() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
readCharacteristic(bodySensorLocationCharacteristic)
.with(bodySensorLocationDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Body Sensor Location characteristic not found")
}
.enqueue()
setNotificationCallback(heartRateCharacteristic) scope.launch {
.with(heartRateMeasurementDataCallback) val data = readCharacteristic(bodySensorLocationCharacteristic)
enableNotifications(heartRateCharacteristic).enqueue() .suspendForValidResponse<BodySensorLocationResponse>()
dataHolder.setSensorLocation(data.sensorLocation)
}
setNotificationCallback(heartRateCharacteristic).asValidResponseFlow<HeartRateMeasurementResponse>()
.onEach {
dataHolder.addNewHeartRate(it.heartRate)
}.launchIn(scope)
scope.launch {
enableNotifications(heartRateCharacteristic).suspend()
}
} }
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
@@ -111,19 +91,14 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
super.isOptionalServiceSupported(gatt) super.isOptionalServiceSupported(gatt)
val service = gatt.getService(HRS_SERVICE_UUID) val service = gatt.getService(HRS_SERVICE_UUID)
if (service != null) { if (service != null) {
bodySensorLocationCharacteristic = service.getCharacteristic( bodySensorLocationCharacteristic = service.getCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)
BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID
)
} }
return bodySensorLocationCharacteristic != null return bodySensorLocationCharacteristic != null
} }
override fun onDeviceDisconnected() { override fun onServicesInvalidated() {
super.onDeviceDisconnected()
bodySensorLocationCharacteristic = null bodySensorLocationCharacteristic = null
heartRateCharacteristic = null heartRateCharacteristic = null
} }
override fun onServicesInvalidated() {}
} }
} }

View File

@@ -13,17 +13,11 @@ internal class HRSService : ForegroundBleService() {
@Inject @Inject
lateinit var repository: HRSRepository lateinit var repository: HRSRepository
override val manager: HRSManager by lazy { HRSManager(this, repository) } override val manager: HRSManager by lazy { HRSManager(this, scope, repository) }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach { repository.command.onEach {
stopSelf() stopSelf()
}.launchIn(scope) }.launchIn(scope)

View File

@@ -11,8 +11,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
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.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun HRSScreen() { fun HRSScreen() {
@@ -25,10 +23,10 @@ fun HRSScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> HRSContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> HRSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -24,11 +24,11 @@ internal class HRSViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -40,16 +40,11 @@ import java.util.*
val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb")
private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb")
/**
* [HTSManager] class performs [BluetoothGatt] operations for connection, service discovery,
* enabling indication and reading characteristics. All operations required to connect to device
* with BLE HT Service and reading health thermometer values are performed here.
*/
internal class HTSManager internal constructor( internal class HTSManager internal constructor(
context: Context, context: Context,
private val scope: CoroutineScope, scope: CoroutineScope,
private val dataHolder: HTSRepository private val dataHolder: HTSRepository
) : BatteryManager(context) { ) : BatteryManager(context, scope) {
private var htCharacteristic: BluetoothGattCharacteristic? = null private var htCharacteristic: BluetoothGattCharacteristic? = null
@@ -65,10 +60,6 @@ internal class HTSManager internal constructor(
return HTManagerGattCallback() return HTManagerGattCallback()
} }
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc..
*/
private inner class HTManagerGattCallback : BatteryManagerGattCallback() { private inner class HTManagerGattCallback : BatteryManagerGattCallback() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()

View File

@@ -18,12 +18,6 @@ internal class HTSService : ForegroundBleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach { repository.command.onEach {
stopSelf() stopSelf()
}.launchIn(scope) }.launchIn(scope)

View File

@@ -11,8 +11,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.hts.R import no.nordicsemi.android.hts.R
import no.nordicsemi.android.hts.viewmodel.HTSViewModel import no.nordicsemi.android.hts.viewmodel.HTSViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun HTSScreen() { fun HTSScreen() {
@@ -25,10 +23,10 @@ fun HTSScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> HTSContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> HTSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -24,11 +24,11 @@ internal class HTSViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -43,9 +43,9 @@ val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-0
internal class PRXManager( internal class PRXManager(
context: Context, context: Context,
private val scope: CoroutineScope, scope: CoroutineScope,
private val dataHolder: PRXRepository private val dataHolder: PRXRepository
) : BatteryManager(context) { ) : BatteryManager(context, scope) {
private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null
private var linkLossCharacteristic: BluetoothGattCharacteristic? = null private var linkLossCharacteristic: BluetoothGattCharacteristic? = null

View File

@@ -1,12 +1,9 @@
package no.nordicsemi.android.prx.repository package no.nordicsemi.android.prx.repository
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.prx.data.* import no.nordicsemi.android.prx.data.*
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.BleServiceStatus
import no.nordicsemi.android.service.ForegroundBleService import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@@ -33,23 +30,23 @@ internal class PRXService : ForegroundBleService() {
serverManager.open() serverManager.open()
status.onEach { // status.onEach {
val bleStatus = when (it) { // val bleStatus = when (it) {
BleServiceStatus.CONNECTING -> BleManagerStatus.CONNECTING // BleServiceStatus.CONNECTING -> BleManagerStatus.CONNECTING
BleServiceStatus.OK -> BleManagerStatus.OK // BleServiceStatus.OK -> BleManagerStatus.OK
BleServiceStatus.DISCONNECTED -> { // BleServiceStatus.DISCONNECTED -> {
scope.close() // scope.close()
stopSelf() // stopSelf()
BleManagerStatus.DISCONNECTED // BleManagerStatus.DISCONNECTED
} // }
BleServiceStatus.LINK_LOSS -> null // BleServiceStatus.LINK_LOSS -> null
}.exhaustive // }.exhaustive
bleStatus?.let { repository.setNewStatus(it) } // bleStatus?.let { repository.setNewStatus(it) }
//
if (BleServiceStatus.LINK_LOSS == it) { // if (BleServiceStatus.LINK_LOSS == it) {
repository.setLocalAlarmLevel(repository.data.value.linkLossAlarmLevel) // repository.setLocalAlarmLevel(repository.data.value.linkLossAlarmLevel)
} // }
}.launchIn(scope) // }.launchIn(scope)
repository.command.onEach { repository.command.onEach {
when (it) { when (it) {

View File

@@ -12,8 +12,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.prx.R import no.nordicsemi.android.prx.R
import no.nordicsemi.android.prx.viewmodel.PRXViewModel import no.nordicsemi.android.prx.viewmodel.PRXViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun PRXScreen() { fun PRXScreen() {
@@ -26,10 +24,10 @@ fun PRXScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> ContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> ContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -27,11 +27,11 @@ internal class PRXViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -42,9 +42,9 @@ private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000
internal class RSCSManager internal constructor( internal class RSCSManager internal constructor(
context: Context, context: Context,
private val scope: CoroutineScope, scope: CoroutineScope,
private val dataHolder: RSCSRepository private val dataHolder: RSCSRepository
) : BatteryManager(context) { ) : BatteryManager(context, scope) {
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
@@ -83,11 +83,6 @@ internal class RSCSManager internal constructor(
} }
override fun onServicesInvalidated() { override fun onServicesInvalidated() {
}
override fun onDeviceDisconnected() {
super.onDeviceDisconnected()
rscMeasurementCharacteristic = null rscMeasurementCharacteristic = null
} }
} }

View File

@@ -18,11 +18,11 @@ internal class RSCSService : ForegroundBleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach { // status.onEach {
val status = it.mapToSimpleManagerStatus() // val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status) // repository.setNewStatus(status)
stopIfDisconnected(status) // stopIfDisconnected(status)
}.launchIn(scope) // }.launchIn(scope)
repository.command.onEach { repository.command.onEach {
stopSelf() stopSelf()

View File

@@ -11,8 +11,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
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.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun RSCSScreen() { fun RSCSScreen() {
@@ -25,10 +23,10 @@ fun RSCSScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> RSCSContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> RSCSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -27,11 +27,11 @@ internal class RSCSViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -45,9 +45,9 @@ private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0
internal class UARTManager( internal class UARTManager(
context: Context, context: Context,
private val scope: CoroutineScope, scope: CoroutineScope,
private val dataHolder: UARTRepository private val dataHolder: UARTRepository
) : BatteryManager(context) { ) : BatteryManager(context, scope) {
private var rxCharacteristic: BluetoothGattCharacteristic? = null private var rxCharacteristic: BluetoothGattCharacteristic? = null
private var txCharacteristic: BluetoothGattCharacteristic? = null private var txCharacteristic: BluetoothGattCharacteristic? = null

View File

@@ -21,11 +21,11 @@ internal class UARTService : ForegroundBleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
status.onEach { // status.onEach {
val status = it.mapToSimpleManagerStatus() // val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status) // repository.setNewStatus(status)
stopIfDisconnected(status) // stopIfDisconnected(status)
}.launchIn(scope) // }.launchIn(scope)
repository.command.onEach { repository.command.onEach {
when (it) { when (it) {

View File

@@ -9,10 +9,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.viewmodel.UARTViewModel import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun UARTScreen() { fun UARTScreen() {
@@ -25,10 +23,10 @@ fun UARTScreen() {
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { // when (state) {
is DisplayDataState -> UARTContentView(state.data) { viewModel.onEvent(it) } // is DisplayDataState -> UARTContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView() // LoadingState -> DeviceConnectingView()
}.exhaustive // }.exhaustive
} }
} }
} }

View File

@@ -26,11 +26,11 @@ internal class UARTViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> val state = repository.data.combine(repository.status) { data, status ->
when (status) { // when (status) {
BleManagerStatus.CONNECTING -> LoadingState // BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK, // BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data) // BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
} // }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {

View File

@@ -13,10 +13,14 @@ dependencyResolutionManagement {
libs { libs {
alias('nordic-ble-common').to('no.nordicsemi.android:ble-common:2.4.0-beta01') alias('nordic-ble-common').to('no.nordicsemi.android:ble-common:2.4.0-beta01')
alias('nordic-ble-ktx').to('no.nordicsemi.android:ble-ktx:2.4.0-beta01') alias('nordic-ble-ktx').to('no.nordicsemi.android:ble-ktx:2.4.0-beta01')
alias('nordic-log').to('no.nordicsemi.android:log:2.3.0')
alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.6.0') alias('nordic-scanner').to('no.nordicsemi.android.support.v18:scanner:1.6.0')
alias('nordic-dfu').to('no.nordicsemi.android:dfu:1.12.1-beta01') alias('nordic-dfu').to('no.nordicsemi.android:dfu:1.12.1-beta01')
alias('nordic-log').to('no.nordicsemi.android:log-timber:2.3.0')
alias('timber-main').to('com.jakewharton.timber:timber:5.0.1')
alias('timber-arcao').to('com.arcao:slf4j-timber:3.1')
bundle('icons', ['nordic-log', 'timber-main', 'timber-arcao'])
version('commonlibraries', '1.0.1') version('commonlibraries', '1.0.1')
alias('nordic-ui-scanner').to('no.nordicsemi.android.common', 'uiscanner').versionRef('commonlibraries') alias('nordic-ui-scanner').to('no.nordicsemi.android.common', 'uiscanner').versionRef('commonlibraries')
alias('nordic-navigation').to('no.nordicsemi.android.common', 'navigation').versionRef('commonlibraries') alias('nordic-navigation').to('no.nordicsemi.android.common', 'navigation').versionRef('commonlibraries')