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

View File

@@ -31,8 +31,11 @@
package no.nordicsemi.android.gls.data 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 records: List<GLSRecord> = emptyList(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
val connectionState: GattConnectionState? = null,
val requestStatus: RequestStatus = RequestStatus.IDLE 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.compose.ui.unit.dp
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.data.GLSData import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.gls.data.RequestStatus
import no.nordicsemi.android.gls.data.WorkingMode 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 import no.nordicsemi.android.ui.view.SectionTitle
@Composable @Composable
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { internal fun GLSContentView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@@ -97,7 +97,7 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni
} }
@Composable @Composable
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { private fun SettingsView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) {
ScreenSection { ScreenSection {
SectionTitle(icon = Icons.Default.Settings, title = "Request items") SectionTitle(icon = Icons.Default.Settings, title = "Request items")
@@ -121,7 +121,7 @@ private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit)
} }
@Composable @Composable
private fun RecordsView(state: GLSData) { private fun RecordsView(state: GLSServiceData) {
ScreenSection { ScreenSection {
if (state.records.isEmpty()) { if (state.records.isEmpty()) {
RecordsViewWithoutData() RecordsViewWithoutData()
@@ -133,7 +133,7 @@ private fun RecordsView(state: GLSData) {
} }
@Composable @Composable
private fun RecordsViewWithData(state: GLSData) { private fun RecordsViewWithData(state: GLSServiceData) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
SectionTitle(resId = R.drawable.ic_records, title = "Records") SectionTitle(resId = R.drawable.ic_records, title = "Records")

View File

@@ -31,10 +31,19 @@
package no.nordicsemi.android.gls.main.view package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData import no.nordicsemi.android.gls.data.GLSServiceData
import no.nordicsemi.android.service.BleManagerResult 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() fun copyAndClear(): GLSViewState {
internal object NoDeviceState : 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 package no.nordicsemi.android.gls.main.viewmodel
import android.annotation.SuppressLint
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.os.ParcelUuid import android.os.ParcelUuid
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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.analytics.Profile import no.nordicsemi.android.analytics.Profile
import no.nordicsemi.android.analytics.ProfileConnectedEvent 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.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.gls.GlsDetailsDestinationId import no.nordicsemi.android.gls.GlsDetailsDestinationId
import no.nordicsemi.android.gls.data.GLS_SERVICE_UUID 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.DisconnectEvent
import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent
import no.nordicsemi.android.gls.main.view.GLSViewState 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.OnGLSRecordClick
import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected
import no.nordicsemi.android.gls.main.view.OpenLoggerEvent 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.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.service.ConnectedResult
import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
import javax.inject.Inject 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 @HiltViewModel
internal class GLSViewModel @Inject constructor( internal class GLSViewModel @Inject constructor(
private val repository: GLSRepository, @ApplicationContext
private val context: Context,
private val navigationManager: Navigator, private val navigationManager: Navigator,
private val analytics: AppAnalytics private val analytics: AppAnalytics
) : ViewModel() { ) : 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() val state = _state.asStateFlow()
init { init {
@@ -81,7 +115,7 @@ internal class GLSViewModel @Inject constructor(
private fun handleResult(result: NavigationResult<ServerDevice>) { private fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) { when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp() 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) { private fun connectDevice(device: ServerDevice) {
repository.downloadData(viewModelScope, device).onEach { repository.downloadData(viewModelScope, device).onEach {
_state.value = WorkingState(it) _state.value = WorkingState(it)
@@ -104,4 +143,66 @@ internal class GLSViewModel @Inject constructor(
} }
}.launchIn(viewModelScope) }.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 package no.nordicsemi.android.gls.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped 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 import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
internal class GLSRepository @Inject constructor( 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()
}
}
} }