mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Migrate GLS profile
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user