Migrate GLS profile

This commit is contained in:
Sylwester Zielinski
2023-03-15 12:07:37 +01:00
parent 3c3f1b2c8b
commit 2ce7303bda
6 changed files with 134 additions and 92 deletions

View File

@@ -60,8 +60,7 @@ private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9
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")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class GLSManager(
context: Context,
@@ -74,8 +73,8 @@ internal class GLSManager(
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
private val data = MutableStateFlow(GLSData())
val dataHolder = ConnectionObserverAdapter<GLSData>()
private val data = MutableStateFlow(GLSServiceData())
val dataHolder = ConnectionObserverAdapter<GLSServiceData>()
init {
connectionObserver = dataHolder

View File

@@ -31,8 +31,11 @@
package no.nordicsemi.android.gls.data
internal data class GLSData(
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
internal data class GLSServiceData(
val records: List<GLSRecord> = emptyList(),
val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null,
val requestStatus: RequestStatus = RequestStatus.IDLE
)

View File

@@ -57,7 +57,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.data.RequestStatus
import no.nordicsemi.android.gls.data.WorkingMode
@@ -67,7 +67,7 @@ import no.nordicsemi.android.ui.view.ScreenSection
import no.nordicsemi.android.ui.view.SectionTitle
@Composable
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
internal fun GLSContentView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
@@ -97,7 +97,7 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni
}
@Composable
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
private fun SettingsView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) {
ScreenSection {
SectionTitle(icon = Icons.Default.Settings, title = "Request items")
@@ -121,7 +121,7 @@ private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit)
}
@Composable
private fun RecordsView(state: GLSData) {
private fun RecordsView(state: GLSServiceData) {
ScreenSection {
if (state.records.isEmpty()) {
RecordsViewWithoutData()
@@ -133,7 +133,7 @@ private fun RecordsView(state: GLSData) {
}
@Composable
private fun RecordsViewWithData(state: GLSData) {
private fun RecordsViewWithData(state: GLSServiceData) {
Column(modifier = Modifier.fillMaxWidth()) {
SectionTitle(resId = R.drawable.ic_records, title = "Records")

View File

@@ -31,10 +31,19 @@
package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.gls.data.RequestStatus
internal sealed class GLSViewState
internal data class GLSViewState(
val glsServiceData: GLSServiceData = GLSServiceData(),
val deviceName: String? = null
) {
internal data class WorkingState(val result: BleManagerResult<GLSData>) : GLSViewState()
internal object NoDeviceState : GLSViewState()
fun copyAndClear(): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(records = emptyList(), requestStatus = RequestStatus.IDLE))
}
fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState {
return copy(glsServiceData = glsServiceData.copy(requestStatus = requestStatus))
}
}

View File

@@ -31,43 +31,77 @@
package no.nordicsemi.android.gls.main.viewmodel
import android.annotation.SuppressLint
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.os.ParcelUuid
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.gls.GlsDetailsDestinationId
import no.nordicsemi.android.gls.data.GLS_SERVICE_UUID
import no.nordicsemi.android.gls.data.RequestStatus
import no.nordicsemi.android.gls.main.view.DisconnectEvent
import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent
import no.nordicsemi.android.gls.main.view.GLSViewState
import no.nordicsemi.android.gls.main.view.NoDeviceState
import no.nordicsemi.android.gls.main.view.OnGLSRecordClick
import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected
import no.nordicsemi.android.gls.main.view.OpenLoggerEvent
import no.nordicsemi.android.gls.main.view.WorkingState
import no.nordicsemi.android.gls.repository.GLSRepository
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattCharacteristic
import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser
import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData
import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSDataParser
import no.nordicsemi.android.service.ConnectedResult
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
import javax.inject.Inject
val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
private val GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-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 BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
@SuppressLint("MissingPermission")
@HiltViewModel
internal class GLSViewModel @Inject constructor(
private val repository: GLSRepository,
@ApplicationContext
private val context: Context,
private val navigationManager: Navigator,
private val analytics: AppAnalytics
) : ViewModel() {
private val _state = MutableStateFlow<GLSViewState>(NoDeviceState)
private lateinit var client: BleGattClient
private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic
private lateinit var glucoseMeasurementContextCharacteristic: BleGattCharacteristic
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
private val _state = MutableStateFlow(GLSViewState())
val state = _state.asStateFlow()
init {
@@ -81,7 +115,7 @@ internal class GLSViewModel @Inject constructor(
private fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> connectDevice(result.value)
is NavigationResult.Success -> onDeviceSelected(result.value)
}
}
@@ -95,6 +129,11 @@ internal class GLSViewModel @Inject constructor(
}
}
private fun onDeviceSelected(device: ServerDevice) {
_state.value = _state.value.copy(deviceName = device.name)
startGattClient(device)
}
private fun connectDevice(device: ServerDevice) {
repository.downloadData(viewModelScope, device).onEach {
_state.value = WorkingState(it)
@@ -104,4 +143,66 @@ internal class GLSViewModel @Inject constructor(
}
}.launchIn(viewModelScope)
}
private fun startGattClient(blinkyDevice: ServerDevice) = viewModelScope.launch {
client = blinkyDevice.connect(context)
client.connectionState
.onEach { _state.value = _state.value.copy() }
.filterNotNull()
.onEach { stopIfDisconnected(it) }
.launchIn(viewModelScope)
client.services
.filterNotNull()
.onEach { configureGatt(it) }
.launchIn(viewModelScope)
}
private suspend fun configureGatt(services: BleGattServices) {
val glsService = services.findService(GLS_SERVICE_UUID)!!
glucoseMeasurementCharacteristic = glsService.findCharacteristic(GM_CHARACTERISTIC)!!
glucoseMeasurementContextCharacteristic = glsService.findCharacteristic(GM_CONTEXT_CHARACTERISTIC)!!
recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
val batteryService = services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
batteryLevelCharacteristic.getNotifications()
.mapNotNull { BatteryLevelParser.parse(it) }
.onEach { repository.onBatteryLevelChanged(it) }
.launchIn(viewModelScope)
htsMeasurementCharacteristic.getNotifications()
.mapNotNull { HRSDataParser.parse(it) }
.onEach { repository.onHRSDataChanged(it) }
.launchIn(viewModelScope)
}
private fun stopIfDisconnected(connectionState: GattConnectionState) {
if (connectionState == GattConnectionState.STATE_DISCONNECTED) {
stopSelf()
}
}
private fun clear() {
_state.value = _state.value.copyAndClear()
}
suspend fun requestLastRecord() {
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord().value)
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
}
suspend fun requestFirstRecord() {
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
}
suspend fun requestAllRecords() {
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
clear()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING)
}
}

View File

@@ -31,82 +31,12 @@
package no.nordicsemi.android.gls.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
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.common.logger.NordicLogger
import no.nordicsemi.android.common.logger.NordicLoggerFactory
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.data.GLSManager
import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.ui.view.StringConst
import javax.inject.Inject
@ViewModelScoped
internal class GLSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val loggerFactory: NordicLoggerFactory,
private val stringConst: StringConst
) {
private var manager: GLSManager? = null
private var logger: NordicLogger? = null
fun downloadData(scope: CoroutineScope, device: ServerDevice): Flow<BleManagerResult<GLSData>> = callbackFlow {
val createdLogger = loggerFactory.create(stringConst.APP_NAME, "GLS", device.address).also {
logger = it
}
val managerInstance = manager ?: GLSManager(context, scope, createdLogger)
manager = managerInstance
managerInstance.dataHolder.status.onEach {
send(it)
}.launchIn(scope)
scope.launch {
managerInstance.start(device)
}
awaitClose {
launch {
manager?.disconnect()?.suspend()
logger = null
manager = null
}
}
}
private suspend fun GLSManager.start(device: ServerDevice) {
// try {
// connect(device.device)
// .useAutoConnect(false)
// .retry(3, 100)
// .suspend()
// } catch (e: Exception) {
// e.printStackTrace()
// }
}
fun openLogger() {
NordicLogger.launch(context, logger)
}
fun requestMode(workingMode: WorkingMode) {
when (workingMode) {
WorkingMode.ALL -> manager?.requestAllRecords()
WorkingMode.LAST -> manager?.requestLastRecord()
WorkingMode.FIRST -> manager?.requestFirstRecord()
}
}
}