From 0d7019a7be08a091d14d431739d52d639ae41b3a Mon Sep 17 00:00:00 2001 From: Vineyro Date: Tue, 28 Mar 2023 12:13:05 +0700 Subject: [PATCH] Fix bug GATT_FAILURE --- .../thermometer/view/TemperatureHistory.kt | 193 +++++++++++------- .../llc/arma/ble/data/BleRepositoryImpl.kt | 170 ++++++++++++--- .../arma/ble/domain/common/BleException.kt | 2 + 3 files changed, 263 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt index 815172c..7abe4cd 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt @@ -37,6 +37,7 @@ import kotlin.random.nextInt import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Refresh import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.style.TextAlign import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter @@ -73,7 +74,7 @@ fun TemperatureHistory( viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial)) } - Column() { + Column { Row( modifier = Modifier.padding(horizontal = 12.dp), @@ -90,7 +91,10 @@ fun TemperatureHistory( onClick = { viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial)) }, - enabled = state.loadingHistoryState is ProgressState.Finished + enabled = when(state){ + is TemperatureHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished + TemperatureHistoryContract.State.Exception -> true + } ) { Icon( imageVector = Icons.Rounded.Refresh, @@ -102,71 +106,67 @@ fun TemperatureHistory( Spacer(modifier = Modifier.height(16.dp)) - when (state.loadingHistoryState) { - is ProgressState.Finished -> { - - Text(text = "${state.loadingHistoryState.data.size}") - - val producer = state.loadingHistoryState.data.mapIndexed { index, measurePoint -> - TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let { - ChartEntryModelProducer(it) + when(state){ + is TemperatureHistoryContract.State.Display -> Display(state = state) + TemperatureHistoryContract.State.Exception -> Exception() + } + + } + +} + +@Composable +fun Display( + state: TemperatureHistoryContract.State.Display +) { + when (state.loadingHistoryState) { + is ProgressState.Finished -> { + + Text(text = "${state.loadingHistoryState.data.size}") + + val producer = state.loadingHistoryState.data.mapIndexed { index, measurePoint -> + TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let { + ChartEntryModelProducer(it) + } + + val axisValueFormatter = AxisValueFormatter { value, chartValues -> + (chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry) + ?.localDate + ?.let { formatter.format(Date(it)) } + .orEmpty() + } + + val lineChart = lineChart( + spacing = 110.dp + ) + + Box(modifier = Modifier.padding(8.dp)) { + + val scrollState = rememberChartScrollState() + + LaunchedEffect(scrollState.maxValue){ + scrollState.scrollBy(scrollState.maxValue) } - val axisValueFormatter = AxisValueFormatter { value, chartValues -> - (chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry) - ?.localDate - ?.let { formatter.format(Date(it)) } - .orEmpty() - } - - val lineChart = lineChart( - spacing = 110.dp + Chart( + chartScrollState = scrollState, + chart = lineChart, + chartModelProducer = producer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + valueFormatter = axisValueFormatter, + labelRotationDegrees = 0f, + ), + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.5f), ) - Box(modifier = Modifier.padding(8.dp)) { - - val scrollState = rememberChartScrollState() - - LaunchedEffect(scrollState.maxValue){ - scrollState.scrollBy(scrollState.maxValue) - } - - Chart( - chartScrollState = scrollState, - chart = lineChart, - chartModelProducer = producer, - startAxis = startAxis(), - bottomAxis = bottomAxis( - valueFormatter = axisValueFormatter, - labelRotationDegrees = 0f, - ), - modifier = Modifier - .fillMaxWidth() - .aspectRatio(1.5f), - ) - - } } - is ProgressState.Indeterminate -> { - - Box(modifier = Modifier.padding(8.dp)) { + } + is ProgressState.Indeterminate -> { - Box( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(2f), - ){ - - CircularProgressIndicator( - strokeCap = StrokeCap.Round, - modifier = Modifier.align(Alignment.Center) - ) - - } - - } - } - is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) { + Box(modifier = Modifier.padding(8.dp)) { Box( modifier = Modifier @@ -174,15 +174,8 @@ fun TemperatureHistory( .aspectRatio(2f), ){ - val progressAnimDuration = 1500 - val progressAnimation by animateFloatAsState( - targetValue = state.loadingHistoryState.value, - animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing) - ) - CircularProgressIndicator( strokeCap = StrokeCap.Round, - progress = progressAnimation, modifier = Modifier.align(Alignment.Center) ) @@ -190,6 +183,48 @@ fun TemperatureHistory( } } + is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) { + + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f), + ){ + + val progressAnimDuration = 1500 + val progressAnimation by animateFloatAsState( + targetValue = state.loadingHistoryState.value, + animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing) + ) + + CircularProgressIndicator( + strokeCap = StrokeCap.Round, + progress = progressAnimation, + modifier = Modifier.align(Alignment.Center) + ) + + } + + } + } +} + +@Composable +fun Exception( + +) { + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .aspectRatio(2f), + ){ + + Text( + textAlign = TextAlign.Center, + text = "Во время загрузки произошла ошибка", + modifier = Modifier.align(Alignment.Center) + ) } @@ -205,9 +240,15 @@ class TemperatureHistoryContract { } - class State( - val loadingHistoryState : ProgressState> - ) : ViewState + sealed class State : ViewState { + + data class Display( + val loadingHistoryState : ProgressState> + ) : State() + + object Exception : State() + + } sealed class Effect : ViewSideEffect { @@ -222,7 +263,7 @@ class TemperatureHistoryViewModel @Inject constructor( private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial ) : BaseViewModel() { - override fun setInitialState() = TemperatureHistoryContract.State( + override fun setInitialState() = TemperatureHistoryContract.State.Display( ProgressState.Indeterminate ) @@ -239,18 +280,20 @@ class TemperatureHistoryViewModel @Inject constructor( viewModelScope.launch { setState { - TemperatureHistoryContract.State(ProgressState.Indeterminate) + TemperatureHistoryContract.State.Display(ProgressState.Indeterminate) } getTemperatureHistoryBySerial(event.serial).onEach { it.fold( onSuccess = { setState { - TemperatureHistoryContract.State(it) + TemperatureHistoryContract.State.Display(it) } }, onFailure = { - + setState { + TemperatureHistoryContract.State.Exception + } } ) }.launchIn(this) diff --git a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt index a05c74d..c0a5a86 100644 --- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt +++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt @@ -4,10 +4,12 @@ import android.Manifest import android.app.Application import android.bluetooth.* import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.pm.PackageManager import android.os.Build +import android.util.Log import androidx.core.app.ActivityCompat import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose @@ -25,6 +27,7 @@ import java.util.* import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine @Singleton class BleRepositoryImpl @Inject constructor( @@ -316,7 +319,15 @@ class BleRepositoryImpl @Inject constructor( ((this[idx + 1].toUInt() and 0xFFu) shl 8) or (this[idx].toUInt() and 0xFFu) - deviceCache[serial]?.device?.let { device -> + findDeviceBySerial(serial).fold( + onSuccess = { + return@fold it + }, + onFailure = { + emit(Result.failure(it)) + return@flow + } + ).let { device -> emit(Result.success(ProgressState.Indeterminate)) @@ -330,7 +341,7 @@ class BleRepositoryImpl @Inject constructor( val countDataArray = readCharacteristic( device = device, serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), - characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") ).fold( onFailure = { emit(Result.failure(it)) @@ -355,7 +366,7 @@ class BleRepositoryImpl @Inject constructor( val firstPackageResponse = readCharacteristic( device = device, serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), - characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") ).fold( onFailure = { emit(Result.failure(it)) @@ -397,7 +408,7 @@ class BleRepositoryImpl @Inject constructor( val readResponse = readCharacteristic( device = device, serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), - characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") ).fold( onFailure = { emit(Result.failure(it)) @@ -406,20 +417,34 @@ class BleRepositoryImpl @Inject constructor( onSuccess = { return@fold it } ) - dataCount = readResponse[1].toUByte() + if(readResponse[0] == 251.toByte()) { - temperatureDataArray = readResponse.toList().subList(2, readResponse.size) + dataCount = readResponse[1].toUByte() - temperaturePackage.addAll( - temperatureDataArray.chunked(2).map { - (it[0] + it[1] * 256).toFloat() / 100f - } - ) + temperatureDataArray = readResponse.toList().subList(2, readResponse.size) - emit(Result.success(ProgressState.Progress(totalDataSize.toFloat() / temperaturePackage.size.toFloat()))) + temperaturePackage.addAll( + temperatureDataArray.chunked(2).map { + (it[0] + it[1] * 256).toFloat() / 100f + } + ) + + emit(Result.success(ProgressState.Progress(totalDataSize.toFloat() / temperaturePackage.size.toFloat()))) + + } else { + + emit(Result.failure(BleException.UnexpectedResponse)) + + } } + readCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") + ) + emit( Result.success( ProgressState.Finished( @@ -433,6 +458,10 @@ class BleRepositoryImpl @Inject constructor( ) ) + } else { + + emit(Result.failure(BleException.UnexpectedResponse)) + } } @@ -559,14 +588,19 @@ class BleRepositoryImpl @Inject constructor( characteristicId: UUID ): Result = suspendCancellableCoroutine { + var result: ByteArray? = null + var bleGatt: BluetoothGatt? = null + val callback = object : BluetoothGattCallback() { override fun onConnectionStateChange( - gatt: BluetoothGatt?, + gatt: BluetoothGatt, status: Int, newState: Int ) { + Log.d("read", "onConnectionStateChange $newState $status") + if (newState == BluetoothProfile.STATE_CONNECTED) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( @@ -574,7 +608,7 @@ class BleRepositoryImpl @Inject constructor( Manifest.permission.BLUETOOTH_CONNECT ) == PackageManager.PERMISSION_GRANTED ) { - gatt?.discoverServices() + gatt.discoverServices() } else { it.resume(Result.failure(BleException.PermissionDenied)) } @@ -584,14 +618,16 @@ class BleRepositoryImpl @Inject constructor( } override fun onServicesDiscovered( - gatt: BluetoothGatt?, + gatt: BluetoothGatt, status: Int ) { super.onServicesDiscovered(gatt, status) + Log.d("read", "onServicesDiscovered $status") + if (status == BluetoothGatt.GATT_SUCCESS) { - gatt?.services?.firstOrNull { service -> + gatt.services?.firstOrNull { service -> service.uuid == serviceId }?.characteristics?.firstOrNull { characteristic -> characteristic.uuid == characteristicId @@ -620,6 +656,9 @@ class BleRepositoryImpl @Inject constructor( status: Int ) { super.onCharacteristicRead(gatt, characteristic, value, status) + + Log.d("read", "onCharacteristicRead $status") + if (ActivityCompat.checkSelfPermission( app, Manifest.permission.BLUETOOTH_CONNECT @@ -627,9 +666,17 @@ class BleRepositoryImpl @Inject constructor( ) { it.resume(Result.failure(BleException.PermissionDenied)) }else { - gatt.disconnect() - it.resume(Result.success(value)) + gatt.close() + result = value + if(result != null){ + it.resume(Result.success(result!!)) + } else { + bleGatt?.close() + it.resume(Result.failure(BleException.UnexpectedResponse)) + } + } + } } @@ -641,7 +688,7 @@ class BleRepositoryImpl @Inject constructor( ) { it.resume(Result.failure(BleException.PermissionDenied)) } else { - device.connectGatt(app, true, callback) + bleGatt = device.connectGatt(app, true, callback) } } @@ -653,14 +700,18 @@ class BleRepositoryImpl @Inject constructor( writeData: ByteArray ) = suspendCancellableCoroutine { + var bleGatt: BluetoothGatt? = null + val callback = object : BluetoothGattCallback() { override fun onConnectionStateChange( - gatt: BluetoothGatt?, + gatt: BluetoothGatt, status: Int, newState: Int ) { + Log.d("write", "onConnectionStateChange $newState") + if (newState == BluetoothProfile.STATE_CONNECTED) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( @@ -668,7 +719,13 @@ class BleRepositoryImpl @Inject constructor( Manifest.permission.BLUETOOTH_CONNECT ) == PackageManager.PERMISSION_GRANTED ) { - gatt?.discoverServices() + gatt.discoverServices() + } + + } else { + + if(newState == BluetoothProfile.STATE_DISCONNECTED && status == BluetoothGatt.GATT_FAILURE){ + bleGatt?.close() } } @@ -676,14 +733,14 @@ class BleRepositoryImpl @Inject constructor( } override fun onServicesDiscovered( - gatt: BluetoothGatt?, + gatt: BluetoothGatt, status: Int ) { super.onServicesDiscovered(gatt, status) - + Log.d("write", "onServicesDiscovered $status") if (status == BluetoothGatt.GATT_SUCCESS) { - gatt?.services?.firstOrNull { service -> + gatt.services?.firstOrNull { service -> service.uuid == serviceId }?.characteristics?.firstOrNull { characteristic -> characteristic.uuid == characteristicId @@ -712,10 +769,13 @@ class BleRepositoryImpl @Inject constructor( override fun onCharacteristicWrite( gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic?, + characteristic: BluetoothGattCharacteristic, status: Int ) { super.onCharacteristicWrite(gatt, characteristic, status) + + Log.d("write", "onCharacteristicWrite $status") + if (ActivityCompat.checkSelfPermission( app, Manifest.permission.BLUETOOTH_CONNECT @@ -723,7 +783,7 @@ class BleRepositoryImpl @Inject constructor( ) { return } else { - gatt.disconnect() + gatt.close() it.resume(Unit) } @@ -731,7 +791,63 @@ class BleRepositoryImpl @Inject constructor( } - device.connectGatt(app, true, callback) + bleGatt = device.connectGatt(app, true, callback) + + } + + private suspend fun findDeviceBySerial(serial: String): Result = suspendCancellableCoroutine { + + val bleCallback = object : ScanCallback() { + + override fun onScanResult( + callbackType: Int, + result: ScanResult + ) { + + super.onScanResult(callbackType, result) + + if(it.isActive) { + + if (ActivityCompat.checkSelfPermission( + app, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + ) { + + + it.resume(Result.success(result.device)) + + } else { + CoroutineScope(Dispatchers.IO).launch { + it.resume( + Result.failure(BleException.PermissionDenied) + ) + } + } + + } + + } + + } + + val bleScanner = + app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner + + bleScanner.startScan( + listOf(ScanFilter.Builder().setDeviceAddress(serial).build()), + ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + .setReportDelay(400L) + .build(), + bleCallback) + + it.invokeOnCancellation { + bleScanner.stopScan(bleCallback) + } } diff --git a/app/src/main/java/llc/arma/ble/domain/common/BleException.kt b/app/src/main/java/llc/arma/ble/domain/common/BleException.kt index 07bf6da..e562194 100644 --- a/app/src/main/java/llc/arma/ble/domain/common/BleException.kt +++ b/app/src/main/java/llc/arma/ble/domain/common/BleException.kt @@ -4,4 +4,6 @@ sealed class BleException { object PermissionDenied : BleException() + object UnexpectedResponse : BleException() + } \ No newline at end of file