mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Redesign manager approach
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_data.tryEmit(_data.value.copy(records = newRecords))
|
manager = managerInstance
|
||||||
}
|
|
||||||
|
|
||||||
fun addNewContext(context: MeasurementContext) {
|
managerInstance.dataHolder.status.onEach {
|
||||||
_data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let {
|
trySend(it)
|
||||||
it.context = context
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
launch {
|
||||||
|
manager?.disconnect()?.suspend()
|
||||||
|
manager = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_data.tryEmit(_data.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setRequestStatus(requestStatus: RequestStatus) {
|
fun requestMode(workingMode: WorkingMode) {
|
||||||
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
|
when (workingMode) {
|
||||||
}
|
WorkingMode.ALL -> manager?.requestAllRecords()
|
||||||
|
WorkingMode.LAST -> manager?.requestLastRecord()
|
||||||
fun records() = _data.value.records
|
WorkingMode.FIRST -> manager?.requestFirstRecord()
|
||||||
|
}.exhaustive
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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() {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user