Fix bug GATT_FAILURE

This commit is contained in:
Vineyro 2023-03-28 12:13:05 +07:00
parent ce95316494
commit 0d7019a7be
3 changed files with 263 additions and 102 deletions

View File

@ -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<AxisPosition.Horizontal.Bottom> { 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<AxisPosition.Horizontal.Bottom> { 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<List<Ble.Thermometer.MeasurePoint>>
) : ViewState
sealed class State : ViewState {
data class Display(
val loadingHistoryState : ProgressState<List<Ble.Thermometer.MeasurePoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
@ -222,7 +263,7 @@ class TemperatureHistoryViewModel @Inject constructor(
private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() {
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)

View File

@ -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<ByteArray, BleException> = 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<BluetoothDevice, BleException> = 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)
}
}

View File

@ -4,4 +4,6 @@ sealed class BleException {
object PermissionDenied : BleException()
object UnexpectedResponse : BleException()
}