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
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import androidx.annotation.IntRange
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
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.*
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")
abstract class BatteryManager(
context: Context,
protected val scope: CoroutineScope,
) : BleManager(context) {
abstract class BatteryManager(context: Context, protected val scope: CoroutineScope) : BleManager(context) {
private val TAG = "BLE-MANAGER"
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() {
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() {
override fun initialize() {
readBatteryLevelCharacteristic()
enableBatteryLevelCharacteristicNotifications()
}
@@ -48,12 +85,13 @@ abstract class BatteryManager(
return batteryLevelCharacteristic != null
}
override fun onServicesInvalidated() {
override fun onDeviceDisconnected() {
batteryLevelCharacteristic = null
onBatteryLevelChanged(0)
}
}
fun releaseScope() {
fun release() {
scope.cancel()
}
}

View File

@@ -45,6 +45,7 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
}
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.layout.Column
@@ -19,6 +19,7 @@ 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 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="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_explanation">The mobile is trying to connect to peripheral device.</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.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
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.SuccessDestinationResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
@@ -38,3 +43,12 @@ fun <T> NavController.consumeResult(value: String): T? {
?.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
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
import no.nordicsemi.android.service.BleManagerStatus
import java.util.*
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
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.Singleton
@Singleton
internal class BPSRepository @Inject constructor() {
@ViewModelScoped
internal class BPSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
) {
private val _data = MutableStateFlow(BPSData())
val data: StateFlow<BPSData> = _data
fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
val scope = this
val manager = BPSManager(context, scope)
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
manager.dataHolder.status.onEach {
trySend(it)
}.launchIn(scope)
fun setIntermediateCuffPressure(
cuffPressure: Float,
unit: Int,
pulseRate: Float?,
userID: Int?,
status: BloodPressureTypes.BPMStatus?,
calendar: Calendar?
) {
_data.tryEmit(_data.value.copy(
cuffPressure = cuffPressure,
unit = unit,
pulseRate = pulseRate,
userID = userID,
status = status,
calendar = calendar
))
}
try {
manager.connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
} catch (e: Exception) {
e.printStackTrace()
}
fun setBloodPressureMeasurement(
systolic: Float,
diastolic: Float,
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
awaitClose {
launch {
manager.disconnect().suspend()
}
}
}
}

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

View File

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

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

View File

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

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.cgms.data.CGMRepository
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@@ -21,11 +20,11 @@ internal class CGMService : ForegroundBleService() {
override fun onCreate() {
super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
// status.onEach {
// val status = it.mapToSimpleManagerStatus()
// repository.setNewStatus(status)
// stopIfDisconnected(status)
// }.launchIn(scope)
repository.command.onEach {
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.viewmodel.CGMScreenViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun CGMScreen() {
@@ -25,10 +23,10 @@ fun CGMScreen() {
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}
}

View File

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

View File

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

View File

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

View File

@@ -16,17 +16,11 @@ internal class CSCService : ForegroundBleService() {
@Inject
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() {
super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach {
when (it) {
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.viewmodel.CSCViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun CSCScreen() {
@@ -25,10 +23,10 @@ fun CSCScreen() {
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}
}

View File

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

View File

@@ -6,8 +6,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
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.viewmodel.DFUViewModel
@@ -21,9 +19,9 @@ fun DFUScreen() {
viewModel.onEvent(OnDisconnectButtonClick)
}
when (state) {
is DisplayDataState -> DFUContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> DFUContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}

View File

@@ -37,11 +37,11 @@ internal class DFUViewModel @Inject constructor(
?.run { createInstallingStateWithNewStatus(state, status) }
?: state
}.combine(repository.status) { data, status ->
when (status) {
BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
}
// when (status) {
// BleManagerStatus.CONNECTING -> LoadingState
// BleManagerStatus.OK,
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
// }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
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
* 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.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
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.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.GlucoseMeasurementResponse
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.gls.data.*
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.service.CloseableCoroutineScope
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
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 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(
@ApplicationContext context: Context,
private val repository: GLSRepository
) : BatteryManager(context) {
private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
@ApplicationContext
context: Context,
private val scope: CoroutineScope
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
private val data = MutableStateFlow(GLSData())
val dataHolder = ConnectionObserverAdapter<GLSData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun onBatteryLevelChanged(batteryLevel: Int) {
repository.setNewBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
override fun getGattCallback(): BleManagerGattCallback {
return GlucoseManagerGattCallback()
}
private inner class GlucoseManagerGattCallback : BatteryManagerGattCallback() {
private inner class GlucoseManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
setNotificationCallback(glucoseMeasurementCharacteristic).asValidResponseFlow<GlucoseMeasurementResponse>()
.onEach {
val record = 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
)
repository.addNewRecord(record)
}.launchIn(scope)
.onEach { data.tryEmit(data.value.copy(records = data.value.records + it.toRecord())) }
.launchIn(scope)
setNotificationCallback(glucoseMeasurementContextCharacteristic).asValidResponseFlow<GlucoseMeasurementContextResponse>()
.onEach {
val context = 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
)
repository.addNewContext(context)
val context = it.toMeasurementContext()
data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let {
it.context = context
}
data.tryEmit(data.value)
}.launchIn(scope)
setIndicationCallback(recordAccessControlPointCharacteristic).asValidResponseFlow<RecordAccessControlPointResponse>()
.onEach {
if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords > 0) {
onNumberOfRecordsReceived(it)
} else if(it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) {
} else if (it.isOperationCompleted && it.wereRecordsFound() && it.numberOfRecords == 0) {
onRecordAccessOperationCompletedWithNoRecordsFound(it)
} else if (it.isOperationCompleted && it.wereRecordsFound()) {
onRecordAccessOperationCompleted(it)
@@ -129,15 +111,15 @@ internal class GLSManager @Inject constructor(
}
}.launchIn(scope)
scope.launch(exceptionHandler) {
enableNotifications(glucoseMeasurementCharacteristic).suspend()
}
scope.launch(exceptionHandler) {
enableNotifications(glucoseMeasurementContextCharacteristic).suspend()
}
scope.launch(exceptionHandler) {
enableIndications(recordAccessControlPointCharacteristic).suspend()
}
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(glucoseMeasurementCharacteristic).enqueue()
enableNotifications(glucoseMeasurementContextCharacteristic).enqueue()
enableIndications(recordAccessControlPointCharacteristic).enqueue()
enableNotifications(batteryLevelCharacteristic).enqueue()
}
private fun onRecordAccessOperationCompleted(response: RecordAccessControlPointResponse) {
@@ -145,21 +127,23 @@ internal class GLSManager @Inject constructor(
RecordAccessControlPointDataCallback.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS
}
repository.setRequestStatus(status)
data.tryEmit(data.value.copy(requestStatus = status))
}
private fun onRecordAccessOperationCompletedWithNoRecordsFound(response: RecordAccessControlPointResponse) {
repository.setRequestStatus(RequestStatus.SUCCESS)
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
}
private suspend fun onNumberOfRecordsReceived(response: RecordAccessControlPointResponse) {
if (response.numberOfRecords > 0) {
if (repository.records().isNotEmpty()) {
val sequenceNumber = repository.records()
.last().sequenceNumber + 1 //TODO check if correct
if (data.value.records.isNotEmpty()) {
val sequenceNumber = data.value.records
.last().sequenceNumber + 1
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber),
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
sequenceNumber
),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend()
} else {
@@ -170,26 +154,28 @@ internal class GLSManager @Inject constructor(
).suspend()
}
}
repository.setRequestStatus(RequestStatus.SUCCESS)
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
}
private fun onRecordAccessOperationError(response: RecordAccessControlPointResponse) {
log(Log.WARN, "Record Access operation failed (error ${response.errorCode})")
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 {
repository.setRequestStatus(RequestStatus.FAILED)
data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
}
}
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(GLS_SERVICE_UUID)
if (service != null) {
glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC)
glucoseMeasurementContextCharacteristic = service.getCharacteristic(GM_CONTEXT_CHARACTERISTIC)
recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_CHARACTERISTIC)
gatt.getService(GLS_SERVICE_UUID)?.run {
glucoseMeasurementCharacteristic = getCharacteristic(GM_CHARACTERISTIC)
glucoseMeasurementContextCharacteristic = getCharacteristic(GM_CONTEXT_CHARACTERISTIC)
recordAccessControlPointCharacteristic = 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() {}
@@ -207,10 +193,10 @@ internal class GLSManager @Inject constructor(
}
private fun clear() {
repository.clearRecords()
data.tryEmit(data.value.copy(records = emptyList()))
val target = bluetoothDevice
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
val target = bluetoothDevice ?: return
clear()
repository.setRequestStatus(RequestStatus.PENDING)
scope.launch(exceptionHandler) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportLastStoredRecord(),
@@ -231,8 +217,8 @@ internal class GLSManager @Inject constructor(
fun requestFirstRecord() {
if (recordAccessControlPointCharacteristic == null) return
clear()
repository.setRequestStatus(RequestStatus.PENDING)
scope.launch(exceptionHandler) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportFirstStoredRecord(),
@@ -244,8 +230,8 @@ internal class GLSManager @Inject constructor(
fun requestAllRecords() {
if (recordAccessControlPointCharacteristic == null) return
clear()
repository.setRequestStatus(RequestStatus.PENDING)
scope.launch(exceptionHandler) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
scope.launchWithCatch {
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportNumberOfAllStoredRecords(),
@@ -253,8 +239,4 @@ internal class GLSManager @Inject constructor(
).suspend()
}
}
fun release() {
scope.close()
}
}

View File

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

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
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -10,8 +11,12 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R
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.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
@Composable
@@ -24,10 +29,19 @@ fun GLSScreen() {
viewModel.onEvent(DisconnectEvent)
}
Log.d("AAATESTAAA", "state: $state")
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> GLSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
NoDeviceState -> NoDeviceView()
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
}
}

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
import android.bluetooth.BluetoothDevice
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import no.nordicsemi.android.gls.GlsDetailsDestinationId
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.repository.GLSManager
import no.nordicsemi.android.gls.repository.GLS_SERVICE_UUID
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.getDevice
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
@@ -22,18 +17,12 @@ import javax.inject.Inject
@HiltViewModel
internal class GLSViewModel @Inject constructor(
private val glsManager: GLSManager,
private val repository: GLSRepository,
private val navigationManager: NavigationManager
) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status ->
when (status) {
BleManagerStatus.CONNECTING -> LoadingState
BleManagerStatus.OK,
BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
val state = _state.asStateFlow()
init {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(GLS_SERVICE_UUID))
@@ -43,29 +32,6 @@ internal class GLSViewModel @Inject constructor(
handleArgs(it)
}
}.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) {
@@ -77,38 +43,16 @@ internal class GLSViewModel @Inject constructor(
fun onEvent(event: GLSScreenViewEvent) {
when (event) {
DisconnectEvent -> disconnect()
is OnWorkingModeSelected -> requestData(event.workingMode)
DisconnectEvent -> navigationManager.navigateUp()
is OnWorkingModeSelected -> repository.requestMode(event.workingMode)
is OnGLSRecordClick -> navigationManager.navigateTo(GlsDetailsDestinationId, AnyArgument(event.record))
DisconnectEvent -> navigationManager.navigateUp()
}.exhaustive
}
private fun connectDevice(device: DiscoveredBluetoothDevice) {
glsManager.connect(device.device)
.useAutoConnect(false)
.retry(3, 100)
.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()
repository.downloadData(device.device).onEach {
_state.value = WorkingState(it)
}.launchIn(viewModelScope)
}
}

View File

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

View File

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

View File

@@ -13,17 +13,11 @@ internal class HRSService : ForegroundBleService() {
@Inject
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() {
super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach {
stopSelf()
}.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.viewmodel.HRSViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun HRSScreen() {
@@ -25,10 +23,10 @@ fun HRSScreen() {
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> HRSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> HRSContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}
}

View File

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

View File

@@ -40,16 +40,11 @@ import java.util.*
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")
/**
* [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(
context: Context,
private val scope: CoroutineScope,
scope: CoroutineScope,
private val dataHolder: HTSRepository
) : BatteryManager(context) {
) : BatteryManager(context, scope) {
private var htCharacteristic: BluetoothGattCharacteristic? = null
@@ -65,10 +60,6 @@ internal class HTSManager internal constructor(
return HTManagerGattCallback()
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery,
* receiving indication, etc..
*/
private inner class HTManagerGattCallback : BatteryManagerGattCallback() {
override fun initialize() {
super.initialize()

View File

@@ -18,12 +18,6 @@ internal class HTSService : ForegroundBleService() {
override fun onCreate() {
super.onCreate()
status.onEach {
val status = it.mapToSimpleManagerStatus()
repository.setNewStatus(status)
stopIfDisconnected(status)
}.launchIn(scope)
repository.command.onEach {
stopSelf()
}.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.viewmodel.HTSViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun HTSScreen() {
@@ -25,10 +23,10 @@ fun HTSScreen() {
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> HTSContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> HTSContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}
}

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
package no.nordicsemi.android.prx.repository
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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.utils.exhaustive
import javax.inject.Inject
@@ -33,23 +30,23 @@ internal class PRXService : ForegroundBleService() {
serverManager.open()
status.onEach {
val bleStatus = when (it) {
BleServiceStatus.CONNECTING -> BleManagerStatus.CONNECTING
BleServiceStatus.OK -> BleManagerStatus.OK
BleServiceStatus.DISCONNECTED -> {
scope.close()
stopSelf()
BleManagerStatus.DISCONNECTED
}
BleServiceStatus.LINK_LOSS -> null
}.exhaustive
bleStatus?.let { repository.setNewStatus(it) }
if (BleServiceStatus.LINK_LOSS == it) {
repository.setLocalAlarmLevel(repository.data.value.linkLossAlarmLevel)
}
}.launchIn(scope)
// status.onEach {
// val bleStatus = when (it) {
// BleServiceStatus.CONNECTING -> BleManagerStatus.CONNECTING
// BleServiceStatus.OK -> BleManagerStatus.OK
// BleServiceStatus.DISCONNECTED -> {
// scope.close()
// stopSelf()
// BleManagerStatus.DISCONNECTED
// }
// BleServiceStatus.LINK_LOSS -> null
// }.exhaustive
// bleStatus?.let { repository.setNewStatus(it) }
//
// if (BleServiceStatus.LINK_LOSS == it) {
// repository.setLocalAlarmLevel(repository.data.value.linkLossAlarmLevel)
// }
// }.launchIn(scope)
repository.command.onEach {
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.viewmodel.PRXViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun PRXScreen() {
@@ -26,10 +24,10 @@ fun PRXScreen() {
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) {
is DisplayDataState -> ContentView(state.data) { viewModel.onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
// when (state) {
// is DisplayDataState -> ContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,10 +13,14 @@ dependencyResolutionManagement {
libs {
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-log').to('no.nordicsemi.android:log:2.3.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-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')
alias('nordic-ui-scanner').to('no.nordicsemi.android.common', 'uiscanner').versionRef('commonlibraries')
alias('nordic-navigation').to('no.nordicsemi.android.common', 'navigation').versionRef('commonlibraries')