From 2a43916ecccd23323800e55839bbd86dd7ff8ad5 Mon Sep 17 00:00:00 2001 From: Vineyro Date: Thu, 6 Apr 2023 14:05:48 +0700 Subject: [PATCH] Fix some bugs, improve ui --- app/build.gradle | 2 - .../llc/arma/ble/app/ui/screen/BleInfoView.kt | 15 +- .../screen/connection/ConnectionContract.kt | 3 +- .../screen/connection/ConnectionViewModel.kt | 14 +- .../app/ui/screen/password/view/Loading.kt | 2 + .../screen/thermometer/ThermometerContract.kt | 2 - .../screen/thermometer/ThermometerScreen.kt | 1 + .../thermometer/ThermometerViewModel.kt | 70 +++-- .../screen/thermometer/view/DisplayState.kt | 4 +- .../screen/thermometer/view/IntervalEdit.kt | 134 ++++++++- .../thermometer/view/TemperatureHistory.kt | 7 +- .../app/ui/screen/thermometer/view/Write.kt | 267 ++++++++++-------- .../llc/arma/ble/data/BleRepositoryImpl.kt | 211 ++++++++++---- .../llc/arma/ble/data/ReadHistoryCallback.kt | 37 +-- .../arma/ble/data/WriteThermometerCallback.kt | 245 ++++++++++++++++ .../java/llc/arma/ble/domain/model/Ble.kt | 6 +- .../java/llc/arma/ble/domain/model/BleInfo.kt | 8 +- .../ble/domain/repository/BleRepository.kt | 2 +- .../arma/ble/domain/usecase/GetBleBySerial.kt | 4 +- 19 files changed, 794 insertions(+), 240 deletions(-) create mode 100644 app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt diff --git a/app/build.gradle b/app/build.gradle index 1ae6330..af334b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,8 +76,6 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta" - implementation "com.chargemap.compose:numberpicker:1.0.3" - implementation "com.patrykandpatrick.vico:core:1.6.4" implementation "com.patrykandpatrick.vico:compose:1.6.4" implementation "com.patrykandpatrick.vico:compose-m3:1.6.4" diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt index 647aec4..ef07c68 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt @@ -81,10 +81,23 @@ fun BleInfoView( contentDescription = null ) }, - title = "Заряд аккумулятора", + title = "Заряд батареи", subtitle = "${bleInfo.batteryLevel} %" ) + SpecDivider() + + BleInfoItem( + icon = { + Icon( + imageVector = Icons.Rounded.NetworkCell, + contentDescription = null + ) + }, + title = "Мощность сигнала", + subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала" + ) + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt index f972f34..775096d 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt @@ -6,6 +6,7 @@ import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.app.ui.screen.beacon.BeaconContract import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract +import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.usecase.GetBleBySerial @@ -32,7 +33,7 @@ class ConnectionContract { object Loading : State() data class DisplayException( - val exception: GetBleBySerial.GetBleException + val exception: BleException ) : State() data class Display( diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt index 35638b0..17c6f81 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt @@ -3,6 +3,8 @@ package llc.arma.ble.app.ui.screen.connection import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.mapper.BleMapper @@ -106,11 +108,13 @@ class ConnectionViewModel @Inject constructor( getBleBySerial(serial).fold( onSuccess = { - setState { - ConnectionContract.State.Display( - ble = it - ) - } + it.onEach { + setState { + ConnectionContract.State.Display( + ble = it + ) + } + }.launchIn(viewModelScope) }, onFailure = { diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt index 4e1556f..01067ff 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/view/Loading.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -27,6 +28,7 @@ fun Loading( ) { CircularProgressIndicator( + strokeCap = StrokeCap.Round, modifier = Modifier.align(Alignment.CenterHorizontally) ) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt index c772b8b..3cf8d16 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt @@ -42,8 +42,6 @@ class ThermometerContract { val ble: Ble.Thermometer ) : Event() - data class OnTxChanged(val tx: Int) : Event() - object OnNavigateUpClicked : Event() } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt index 9389907..26f73e6 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt @@ -201,6 +201,7 @@ fun ThermometerScreen( when(state){ is ThermometerContract.State.Display -> { DisplayState( + origin = state.origin, ble = state.thermometer, onEvent = { viewModel.setEvent(it) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt index 1ed2411..50d6323 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt @@ -23,7 +23,6 @@ class ThermometerViewModel @Inject constructor( override fun handleEvents(event: ThermometerContract.Event) { when(event){ is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event) - is ThermometerContract.Event.OnTxChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnBleChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event) @@ -46,24 +45,30 @@ class ThermometerViewModel @Inject constructor( setEffect { ThermometerContract.Effect.Navigation.NavigateUp } } - private fun reduce( - state: ThermometerContract.State, - event: ThermometerContract.Event.OnTxChanged - ) { - - } - private fun reduce( state: ThermometerContract.State, event: ThermometerContract.Event.OnBleChanged ) { - setState { - ThermometerContract.State.Display( - origin = event.ble, - thermometer = bleMapper.map(event.ble) as BleView.Thermometer, - writeState = null - ) + + when(state){ + is ThermometerContract.State.Display -> setState { + state.copy( + origin = Ble.Thermometer( + info = event.ble.info, + state = state.origin.state, + thermometerState = state.origin.thermometerState + ) + ) + } + is ThermometerContract.State.Loading -> setState { + ThermometerContract.State.Display( + origin = event.ble, + thermometer = bleMapper.map(event.ble) as BleView.Thermometer, + writeState = null + ) + } } + } private fun reduce( @@ -188,25 +193,48 @@ class ThermometerViewModel @Inject constructor( if(state is ThermometerContract.State.Display){ - state.writeState?.let { + state.writeState?.let { request -> - if(it is ThermometerContract.State.Display.WriteState.DisplayPreview) { + if(request is ThermometerContract.State.Display.WriteState.DisplayPreview) { viewModelScope.launch { setState { state.copy( - writeState = ThermometerContract.State.Display.WriteState.Writing(it.writeRequest) + writeState = ThermometerContract.State.Display.WriteState.Writing(request.writeRequest) ) } - writeBle(state.thermometer.info.serial, it.writeRequest).fold( + writeBle(state.thermometer.info.serial, request.writeRequest).fold( onSuccess = { - setState { - state.copy( - writeState = ThermometerContract.State.Display.WriteState.Success + + val currentState = viewState.value + + if(currentState is ThermometerContract.State.Display) { + + val newBleObject = Ble.Thermometer( + info = currentState.origin.info, + state = currentState.origin.state.copy( + tx = request.writeRequest.tx ?: state.origin.state.tx + ), + thermometerState = currentState.origin.thermometerState.copy( + saveHistory = request.writeRequest.saveHistory + ?: currentState.origin.thermometerState.saveHistory, + historyInterval = request.writeRequest.historyInterval + ?: currentState.origin.thermometerState.historyInterval, + ) ) + + setState { + currentState.copy( + origin = newBleObject, + thermometer = bleMapper.map(newBleObject) as BleView.Thermometer, + writeState = ThermometerContract.State.Display.WriteState.Success + ) + } + } + }, onFailure = { setState { diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt index 08df045..de1e5f0 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt @@ -19,10 +19,12 @@ import androidx.compose.ui.unit.dp import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.app.ui.screen.BleInfoView import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract +import llc.arma.ble.domain.model.Ble @Composable fun DisplayState( onEvent: (ThermometerContract.Event) -> Unit, + origin: Ble.Thermometer, ble: BleView.Thermometer ) { @@ -40,7 +42,7 @@ fun DisplayState( horizontal = 8.dp ) ) { - BleInfoView(bleInfo = ble.info) + BleInfoView(bleInfo = origin.info) } Column( diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt index 8aef3a3..a4174d4 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt @@ -1,15 +1,16 @@ package llc.arma.ble.app.ui.screen.thermometer.view +import androidx.compose.animation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.chargemap.compose.numberpicker.NumberPicker import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract @@ -23,6 +24,22 @@ fun IntervalEdit( mutableStateOf((state.thermometerState.historyInterval / 1000 / 60 / 60).toInt()) } + val maxInterval = 240 + + if(value > maxInterval){ + value = maxInterval + } + + if(value < 1){ + value = 1 + } + + val maxHours = maxInterval + val maxDays = maxInterval / 24 + + val dayValue = value / 24 + val hourValue = value - (24 * dayValue) + Column( modifier = Modifier ) { @@ -35,28 +52,36 @@ fun IntervalEdit( Spacer(modifier = Modifier.height(16.dp)) + Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.CenterHorizontally) ) { NumberPicker( - dividersColor = MaterialTheme.colorScheme.primary, - value = value, - onValueChange = { - value = it - }, - textStyle = MaterialTheme.typography.titleMedium, - range = 1..100 + range = -1..maxDays, + value = dayValue, + onValueChanged = { value = (it * 24) + hourValue } ) Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "ч.", - style = MaterialTheme.typography.titleMedium + Text(text = "Дни") + + Spacer(modifier = Modifier.width(16.dp)) + + NumberPicker( + range = -1..maxHours, + value = hourValue, + onValueChanged = { + value = it + (dayValue * 24) + } ) + Spacer(modifier = Modifier.width(8.dp)) + + Text(text = "Часы") + } Spacer(modifier = Modifier.height(16.dp)) @@ -92,4 +117,85 @@ fun IntervalEdit( } +} + +@Composable +fun NumberPicker( + modifier: Modifier = Modifier, + range: IntRange, + value: Int, + onValueChanged: (Int) -> Unit +) { + + LaunchedEffect(range){ + + if(value > range.last){ + + onValueChanged(range.last) + + } + + if(value < range.first){ + + onValueChanged(range.first) + + } + + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + ){ + + FilledIconButton( + onClick = { + if(value < range.last) onValueChanged(value + 1) + } + ) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowUp, + contentDescription = null + ) + } + + Spacer(modifier = Modifier.height(36.dp)) + + AnimatedContent( + targetState = value, + transitionSpec = { + if (targetState > initialState) { + slideInVertically { height -> height } + fadeIn() with + slideOutVertically { height -> -height } + fadeOut() + } else { + slideInVertically { height -> -height } + fadeIn() with + slideOutVertically { height -> height } + fadeOut() + }.using( + SizeTransform(clip = false) + ) + } + ) { targetCount -> + Text( + style = MaterialTheme.typography.displaySmall, + text = "$targetCount" + ) + } + + Spacer(modifier = Modifier.height(36.dp)) + + FilledIconButton( + onClick = { + if(value > range.first) onValueChanged(value - 1) + + } + ) { + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + } + + } + + } \ No newline at end of file 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 8f281de..13bc000 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 @@ -278,6 +278,8 @@ class TemperatureHistoryViewModel @Inject constructor( private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial ) : BaseViewModel() { + private var lastSerial: String? = null + override fun setInitialState() = TemperatureHistoryContract.State.Display( ProgressState.Indeterminate ) @@ -297,7 +299,9 @@ class TemperatureHistoryViewModel @Inject constructor( if(state is TemperatureHistoryContract.State.Display) { - if(state.loadingHistoryState is ProgressState.Indeterminate) { + if(lastSerial != event.serial) { + + lastSerial = event.serial setState { TemperatureHistoryContract.State.Display(ProgressState.Indeterminate) @@ -323,6 +327,7 @@ class TemperatureHistoryViewModel @Inject constructor( } } + } private fun reduce( diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt index d23b38b..c22a591 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/Write.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @@ -27,10 +28,8 @@ fun Write( onEvent: (ThermometerContract.Event) -> Unit ) { - val scope = rememberCoroutineScope() - Column( - modifier = Modifier.animateContentSize { initialValue, targetValue -> } + modifier = Modifier.animateContentSize() ) { Text( @@ -44,69 +43,73 @@ fun Write( when (state) { is ThermometerContract.State.Display.WriteState.DisplayPreview -> { - state.writeRequest.tx?.let { - Box( - modifier = Modifier.padding( - vertical = 0.dp, - horizontal = 8.dp - ) - ) { + if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .padding(8.dp) + state.writeRequest.tx?.let { + Box( + modifier = Modifier.padding( + vertical = 0.dp, + horizontal = 8.dp + ) ) { - Column( - modifier = Modifier.weight(1f) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .padding(8.dp) ) { - Text( - text = "Мощность" - ) - Text( - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.bodyMedium, - text = "${it.localizedName} db" - ) + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Мощность" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${it.localizedName} db" + ) + + } } } - } - } - state.writeRequest.saveHistory?.let { + state.writeRequest.saveHistory?.let { - Box( - modifier = Modifier.padding( - vertical = 0.dp, - horizontal = 8.dp - ) - ) { - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .padding(8.dp) + Box( + modifier = Modifier.padding( + vertical = 0.dp, + horizontal = 8.dp + ) ) { - Column( - modifier = Modifier.weight(1f) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .padding(8.dp) ) { - Text( - text = "Сохранять историю измерений" - ) - Text( - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.bodyMedium, - text = "${it.localizedName}" - ) + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Сохранять историю измерений" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${it.localizedName}" + ) + + } } @@ -114,36 +117,36 @@ fun Write( } - } + state.writeRequest.historyInterval?.let { - state.writeRequest.historyInterval?.let { - - Box( - modifier = Modifier.padding( - vertical = 0.dp, - horizontal = 8.dp - ) - ) { - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .padding(8.dp) + Box( + modifier = Modifier.padding( + vertical = 0.dp, + horizontal = 8.dp + ) ) { - Column( - modifier = Modifier.weight(1f) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .padding(8.dp) ) { - Text( - text = "Интервал измерний" - ) - Text( - color = MaterialTheme.colorScheme.secondary, - style = MaterialTheme.typography.bodyMedium, - text = "${it / 1000 / 60 / 60} ч." - ) + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Интервал измерний" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${it / 1000 / 60 / 60} ч." + ) + + } } @@ -151,55 +154,92 @@ fun Write( } - } + Spacer(modifier = Modifier.height(20.dp)) - Spacer(modifier = Modifier.height(20.dp)) + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent(ThermometerContract.Event.OnWriteBle) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .height(50.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer, - onClick = { - onEvent(ThermometerContract.Event.OnWriteBle) - } - ) { + Box(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize()) { + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Записать" + ) - Text( - modifier = Modifier.align(Alignment.Center), - color = MaterialTheme.colorScheme.background, - style = MaterialTheme.typography.labelLarge, - text = "Записать" - ) + } } - } + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + onClick = { + onEvent(ThermometerContract.Event.OnHideWriteBlePreview) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + text = "Отменить" + ) + + } - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .height(50.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant, - onClick = { - onEvent(ThermometerContract.Event.OnHideWriteBlePreview) } - ) { - Box(modifier = Modifier.fillMaxSize()) { + } else { - Text( - modifier = Modifier.align(Alignment.Center), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelLarge, - text = "Отменить" - ) + Spacer(modifier = Modifier.height(38.dp)) + + Text( + text = "Нет изменений", + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(64.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + onClick = { + onEvent(ThermometerContract.Event.OnHideWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } } @@ -216,6 +256,7 @@ fun Write( Spacer(modifier = Modifier.height(28.dp)) CircularProgressIndicator( + strokeCap = StrokeCap.Round, modifier = Modifier .align(Alignment.CenterHorizontally) ) 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 bc7955d..b8e24a4 100644 --- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt +++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt @@ -9,6 +9,7 @@ import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.pm.PackageManager import android.os.Build +import android.os.SystemClock import android.util.Log import androidx.core.app.ActivityCompat import kotlinx.coroutines.* @@ -183,73 +184,134 @@ class BleRepositoryImpl @Inject constructor( } - @OptIn(ExperimentalCoroutinesApi::class) override suspend fun getBleBySerial( serial: String - ): Result = suspendCancellableCoroutine { + ): Result, BleException> { deviceCache[serial]?.let { result -> - if (checkPermission()) { + return when(result.info.type) { + BleInfo.Type.BEACON -> { + Result.success( + flow { - if (it.isActive) { + while (true) { - val info = result.info + deviceCache[serial]?.let { newResult -> - val state = Ble.BleState( - tx = when (result.scanRecord?.txPowerLevel) { - -40 -> Ble.BleState.TX.MINUS_40 - -20 -> Ble.BleState.TX.MINUS_20 - -16 -> Ble.BleState.TX.MINUS_16 - -12 -> Ble.BleState.TX.MINUS_12 - -8 -> Ble.BleState.TX.MINUS_8 - -4 -> Ble.BleState.TX.MINUS_4 - 3 -> Ble.BleState.TX.PLUS_3 - 4 -> Ble.BleState.TX.PLUS_4 - else -> Ble.BleState.TX.ZERO - } - ) + val state = Ble.BleState( + tx = when (result.scanRecord?.txPowerLevel) { + -40 -> Ble.BleState.TX.MINUS_40 + -20 -> Ble.BleState.TX.MINUS_20 + -16 -> Ble.BleState.TX.MINUS_16 + -12 -> Ble.BleState.TX.MINUS_12 + -8 -> Ble.BleState.TX.MINUS_8 + -4 -> Ble.BleState.TX.MINUS_4 + 3 -> Ble.BleState.TX.PLUS_3 + 4 -> Ble.BleState.TX.PLUS_4 + else -> Ble.BleState.TX.ZERO + } + ) - CoroutineScope(Dispatchers.IO).launch { + emit( - val resultValue = when (info.type) { + Ble.Beacon( + info = newResult.info.copy( + rssi = if((SystemClock.elapsedRealtimeNanos() - newResult.timestampNanos) > 15_000_000_000) { + null + } else { + newResult.rssi + } - BleInfo.Type.BEACON -> Ble.Beacon( - info = info, - state = state - ) + ), + state = state + ) - BleInfo.Type.THERMOMETER -> { + ) - val thermometer = readThermometerState(result).fold( - onFailure = { _ -> - return@launch it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) - }, - onSuccess = { return@fold it } - ) + } - Ble.Thermometer( - info = info, - state = state, - thermometerState = thermometer - ) + delay(500) } } + ) + } + BleInfo.Type.THERMOMETER -> { - it.resume(Result.success(resultValue)) {} + val tState = suspendCancellableCoroutine { - } + CoroutineScope(Dispatchers.IO).launch { + + it.resume(readThermometerState(result)) + + } + + }.fold( + onFailure = { + return Result.failure(it) + }, + onSuccess = { + it + } + ) + + Result.success( + flow { + + while (true) { + + deviceCache[serial]?.let { newResult -> + + val state = Ble.BleState( + tx = when (result.scanRecord?.txPowerLevel) { + -40 -> Ble.BleState.TX.MINUS_40 + -20 -> Ble.BleState.TX.MINUS_20 + -16 -> Ble.BleState.TX.MINUS_16 + -12 -> Ble.BleState.TX.MINUS_12 + -8 -> Ble.BleState.TX.MINUS_8 + -4 -> Ble.BleState.TX.MINUS_4 + 3 -> Ble.BleState.TX.PLUS_3 + 4 -> Ble.BleState.TX.PLUS_4 + else -> Ble.BleState.TX.ZERO + } + ) + + emit( + + Ble.Thermometer( + info = newResult.info.copy( + rssi = if((SystemClock.elapsedRealtimeNanos() - newResult.timestampNanos) > 15_000_000_000) { + null + } else { + newResult.rssi + } + + ), + state = state, + thermometerState = tState + ) + + ) + + } + + delay(500) + + } + + } + ) } - } else { - it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) {} } } + return llc.arma.ble.domain.Result.failure(BleException.UnexpectedResponse) + } private suspend fun readThermometerState( @@ -368,38 +430,74 @@ class BleRepositoryImpl @Inject constructor( } } + } override suspend fun writeBle( serial: String, request: Ble.Thermometer.WriteRequest - ): Result { + ): Result = suspendCancellableCoroutine { - deviceCache[serial]?.let { result -> + deviceCache[serial]?.let { scanResult -> - request.tx?.let { writeTx(result.device, it) }?.onFailure { + if(checkPermission()) { + + var gatt: BluetoothGatt? = null + + val callback = WriteThermometerCallback(app, request) { result -> + + gatt?.close() + + result.onSuccess { + deviceCache.remove(serial) + resultList.remove(serial) + } + + it.resume(result) + + } + + gatt = scanResult.device.connectGatt(app, false, callback) + + } else { + + it.resume(Result.failure(BleException.PermissionDenied)) + + } + + /*request.tx?.let { + Log.d("write", "tx") + writeTx(result.device, it) + }?.onFailure { + Log.d("write", "tx fail") return Result.failure(it) } - request.historyInterval?.let { writeSaveInterval(result.device, it) }?.onFailure { + request.historyInterval?.let { + Log.d("write", "in") + writeSaveInterval(result.device, it) + }?.onFailure { + Log.d("write", "in fail") return Result.failure(it) } - request.saveHistory?.let { writeSaveEnabled(result.device, it) }?.onFailure { + request.saveHistory?.let { + Log.d("write", "hs") + writeSaveEnabled(result.device, it) + }?.onFailure { + Log.d("write", "hs fail") return Result.failure(it) } + Log.d("write", "fs") + writeToFlash(serial).onFailure { + Log.d("write", "fs fail") return Result.failure(it) - } - - deviceCache.remove(serial) - resultList.remove(serial) + }*/ } - return Result.success(Unit) - } override suspend fun writeBle( @@ -510,7 +608,7 @@ class BleRepositoryImpl @Inject constructor( serviceId = serviceUUID, characteristicId = intervalWriteUUID, writeData = mutableListOf(3).apply { - addAll(interval.toUInt().to4ByteArrayInBigEndian().toList()) + addAll((interval / 1_000).toUInt().to4ByteArrayInBigEndian().toList()) }.toByteArray() ) @@ -699,7 +797,9 @@ class BleRepositoryImpl @Inject constructor( if (newState == BluetoothProfile.STATE_CONNECTED) { if (checkPermission()) { + gatt.discoverServices() + } else { it.resume(Result.failure(BleException.PermissionDenied)) @@ -708,7 +808,7 @@ class BleRepositoryImpl @Inject constructor( } else { - it.resume(Result.failure(BleException.UnexpectedResponse)) + it.resume(Result.success(Unit)) bleGatt?.close() } @@ -752,10 +852,14 @@ class BleRepositoryImpl @Inject constructor( } + Log.d("write", "service not found") + + gatt.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) } else { + gatt.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) } @@ -787,6 +891,7 @@ class BleRepositoryImpl @Inject constructor( } else { + gatt.close() it.resume(Result.failure(BleException.PermissionDenied)) } diff --git a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt index 660f4a4..ce51983 100644 --- a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt @@ -1,7 +1,6 @@ package llc.arma.ble.data import android.Manifest -import android.annotation.SuppressLint import android.app.Application import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback @@ -13,7 +12,7 @@ import llc.arma.ble.domain.Result import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.model.Ble -import java.util.* +import java.util.stream.Collectors enum class Property { DATA_SIZE, PACKAGE @@ -24,12 +23,16 @@ class ReadHistoryCallback( private val onResult: (Result>, BleException>) -> Unit ) : BluetoothGattCallback() { - private fun ByteArray.getUIntAt(idx: Int) = + private fun ByteArray.get4byteUIntAt(idx: Int) = ((this[idx + 3].toUInt() and 0xFFu) shl 24) or ((this[idx + 2].toUInt() and 0xFFu) shl 16) or ((this[idx + 1].toUInt() and 0xFFu) shl 8) or (this[idx].toUInt() and 0xFFu) + private fun ByteArray.get2byteUIntAt(idx: Int) = + ((this[idx + 1].toUInt() and 0xFFu) shl 8) or + (this[idx].toUInt() and 0xFFu) + private var readProperty: Property? = null init { @@ -122,6 +125,7 @@ class ReadHistoryCallback( onCommonCharacteristicRead(gatt, characteristic, value, status) } + @OptIn(ExperimentalUnsignedTypes::class) private fun onCommonCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, @@ -161,30 +165,29 @@ class ReadHistoryCallback( if(value[0] == 250.toByte()){ - bleMeasureInterval = value.getUIntAt(2).toLong() - bleLastMeasureTime = value.getUIntAt(6).toLong() - bleRealTime = value.getUIntAt(10).toLong() + bleMeasureInterval = value.get4byteUIntAt(4).toLong() + bleLastMeasureTime = value.get4byteUIntAt(8).toLong() + bleRealTime = value.get4byteUIntAt(12).toLong() - lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) / 10_000) + lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) * 1_000) - val temperatureDataArray = value.asList().subList(14, value.size) + val temperatureDataArray = value.toUByteArray().asList().subList(16, value.size) resultTemperaturePackage.addAll( temperatureDataArray.chunked(2).map { - (it[0] + it[1] * 256).toFloat() / 100f + (it[0] + it[1] * 256u).toFloat() / 100f }.toMutableList() ) - val totalDataSize = value[1].toUByte().toInt() + temperatureDataArray.size / 2 + val totalDataSize = value.get2byteUIntAt(2).toInt() + temperatureDataArray.size / 2 - val nextPackageDataCount = value[1].toUByte() + val nextPackageDataCount = value.get2byteUIntAt(2) expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat()))) onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat()))) - if(nextPackageDataCount != 0.toUByte()){ - + if(nextPackageDataCount != 0.toUInt()){ if (checkPermission()) { @@ -218,18 +221,18 @@ class ReadHistoryCallback( if (value[0] == 251.toByte()) { - val nextPackageDataCount = value[1].toUByte() - val temperatureDataArray = value.toList().subList(2, value.size) + val nextPackageDataCount = value.get2byteUIntAt(2) + val temperatureDataArray = value.toUByteArray().toList().subList(4, value.size) resultTemperaturePackage.addAll( temperatureDataArray.chunked(2).map { - (it[0] + it[1] * 256).toFloat() / 100f + (it[0] + it[1] * 256u).toFloat() / 100f } ) onResult(Result.success(ProgressState.Progress(expectedDataSize!!.toFloat() / resultTemperaturePackage.size.toFloat()))) - if (nextPackageDataCount != 0.toUByte()) { + if (nextPackageDataCount != 0.toUInt()) { val writeData = byteArrayOf(5) diff --git a/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt new file mode 100644 index 0000000..a2b81e2 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt @@ -0,0 +1,245 @@ +package llc.arma.ble.data + +import android.Manifest +import android.app.Application +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothProfile +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import llc.arma.ble.domain.Result +import llc.arma.ble.domain.common.BleException +import llc.arma.ble.domain.model.Ble +import java.util.UUID + +class WriteThermometerCallback( + private val app: Application, + private var request: Ble.Thermometer.WriteRequest, + private val onResult: (Result) -> Unit +) : BluetoothGattCallback() { + + private var flashed = false + + override fun onConnectionStateChange( + gatt: BluetoothGatt, + status: Int, + newState: Int + ) { + super.onConnectionStateChange(gatt, status, newState) + + Log.d("th", "onConnectionStateChange $status $newState") + + if(checkPermission()) { + + if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { + + gatt.discoverServices() + + } else { + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + + } + + } + + override fun onServicesDiscovered( + gatt: BluetoothGatt, + status: Int + ) { + Log.d("th", "onServicesDiscovered $status") + super.onServicesDiscovered(gatt, status) + onCycle(gatt, status) + + } + + private fun onCycle( + gatt: BluetoothGatt, + status: Int + ){ + + if(request.tx != null || request.saveHistory != null || request.historyInterval != null) { + + fun UInt.to4ByteArrayInBigEndian(): ByteArray = + (3 downTo 0).map { + (this shr (it * Byte.SIZE_BITS)).toByte() + }.reversed().toByteArray() + + var uuid: Pair? = null + + uuid = request.historyInterval?.let { + + this.request = request.copy( + historyInterval = null + ) + + Pair( + intervalWriteUUID, + mutableListOf(3).apply { + addAll((it).toUInt().to4ByteArrayInBigEndian().toList()) + }.toByteArray() + ) + } + + uuid = request.saveHistory?.let { + + this.request = request.copy( + saveHistory = null + ) + + Pair( + saveEnabledWriteUUID, + mutableListOf(4).apply { + add(if (it) 1 else 0) + }.toByteArray() + ) + } ?: uuid + + uuid = request.tx?.let { + + this.request = request.copy( + tx = null + ) + + Pair( + txWriteUUID, + byteArrayOf( + when (it) { + Ble.BleState.TX.MINUS_40 -> -40 + Ble.BleState.TX.MINUS_20 -> -20 + Ble.BleState.TX.MINUS_16 -> -16 + Ble.BleState.TX.MINUS_12 -> -12 + Ble.BleState.TX.MINUS_8 -> -8 + Ble.BleState.TX.MINUS_4 -> -4 + Ble.BleState.TX.ZERO -> 0 + Ble.BleState.TX.PLUS_3 -> 3 + Ble.BleState.TX.PLUS_4 -> 4 + } + ) + ) + + } ?: uuid + + uuid?.let { uuid -> + + gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull { + it.uuid == uuid.first + }?.let { + + gatt.writeCharacteristic(it, uuid.second) + + return + + } + + } + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } else { + + if(flashed.not()){ + + flashed = true + + gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull { + it.uuid == flashWriteUUID + }?.let { + + gatt.writeCharacteristic(it, byteArrayOf(9)) + + return + + } + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } else { + + onResult(Result.success(Unit)) + + } + + } + + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + + Log.d("th", "onCharacteristicWrite $status") + + super.onCharacteristicWrite(gatt, characteristic, status) + + if(checkPermission()) { + + if(status == BluetoothGatt.GATT_SUCCESS || flashed) { + + onCycle(gatt, status) + + } else { + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + + } + + } + + fun BluetoothGatt.writeCharacteristic( + characteristic: BluetoothGattCharacteristic, + data: ByteArray + ): Result { + + return if(checkPermission()){ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + }else{ + + characteristic.writeType + characteristic.value = data + writeCharacteristic(characteristic) + } + + Result.success(Unit) + + } else { + Result.failure(BleException.PermissionDenied) + } + + } + + fun checkPermission(): Boolean { + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) == + PackageManager.PERMISSION_GRANTED + } else { + return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt index 1ec1cb3..75a52ac 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt @@ -26,13 +26,13 @@ sealed class Ble( val value: Float ) - class ThermometerState( + data class ThermometerState( val temperature: Float, val saveHistory: Boolean, val historyInterval: Long ) - class WriteRequest( + data class WriteRequest( val tx: BleState.TX?, val saveHistory: Boolean?, val historyInterval: Long? @@ -40,7 +40,7 @@ sealed class Ble( } - class BleState( + data class BleState( val tx: TX ){ diff --git a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt index 2699981..6a72a27 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt @@ -2,16 +2,16 @@ package llc.arma.ble.domain.model import java.util.UUID -class BleInfo( +data class BleInfo( val name: String, val serial: String, val batteryLevel: Int, - val rssi: Int, + val rssi: Int?, val type: Type ){ - enum class Type(val serviceUUID: String?) { - BEACON(null), THERMOMETER("a77db03a-9bc4-11ed-a8fc-0242ac120002") + enum class Type { + BEACON, THERMOMETER } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt index 537ecd4..9244462 100644 --- a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt +++ b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt @@ -15,7 +15,7 @@ interface BleRepository { fun getBleAroundFlow(): Flow, BleException>> - suspend fun getBleBySerial(serial: String): Result + suspend fun getBleBySerial(serial: String) : Result, BleException> suspend fun getTemperatureHistoryBySerial(serial: String): Flow>, BleException>> diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt index d9b2ce1..1207671 100644 --- a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt +++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt @@ -1,15 +1,17 @@ package llc.arma.ble.domain.usecase +import kotlinx.coroutines.flow.Flow import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.repository.BleRepository import javax.inject.Inject import llc.arma.ble.domain.Result +import llc.arma.ble.domain.common.BleException class GetBleBySerial @Inject constructor( private val bleRepository: BleRepository ) { - suspend operator fun invoke(serial: String): Result = + suspend operator fun invoke(serial: String): Result, BleException> = bleRepository.getBleBySerial(serial) sealed class GetBleException {