Fix some bugs, improve ui

This commit is contained in:
Vineyro 2023-04-06 14:05:48 +07:00
parent 5e309d001c
commit 2a43916ecc
19 changed files with 794 additions and 240 deletions

View File

@ -76,8 +76,6 @@ dependencies {
implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta" 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:core:1.6.4"
implementation "com.patrykandpatrick.vico:compose:1.6.4" implementation "com.patrykandpatrick.vico:compose:1.6.4"
implementation "com.patrykandpatrick.vico:compose-m3:1.6.4" implementation "com.patrykandpatrick.vico:compose-m3:1.6.4"

View File

@ -81,10 +81,23 @@ fun BleInfoView(
contentDescription = null contentDescription = null
) )
}, },
title = "Заряд аккумулятора", title = "Заряд батареи",
subtitle = "${bleInfo.batteryLevel} %" subtitle = "${bleInfo.batteryLevel} %"
) )
SpecDivider()
BleInfoItem(
icon = {
Icon(
imageVector = Icons.Rounded.NetworkCell,
contentDescription = null
)
},
title = "Мощность сигнала",
subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала"
)
} }
} }

View File

@ -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.model.BleView
import llc.arma.ble.app.ui.screen.beacon.BeaconContract import llc.arma.ble.app.ui.screen.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract 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.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
@ -32,7 +33,7 @@ class ConnectionContract {
object Loading : State() object Loading : State()
data class DisplayException( data class DisplayException(
val exception: GetBleBySerial.GetBleException val exception: BleException
) : State() ) : State()
data class Display( data class Display(

View File

@ -3,6 +3,8 @@ package llc.arma.ble.app.ui.screen.connection
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.mapper.BleMapper
@ -106,11 +108,13 @@ class ConnectionViewModel @Inject constructor(
getBleBySerial(serial).fold( getBleBySerial(serial).fold(
onSuccess = { onSuccess = {
it.onEach {
setState { setState {
ConnectionContract.State.Display( ConnectionContract.State.Display(
ble = it ble = it
) )
} }
}.launchIn(viewModelScope)
}, },
onFailure = { onFailure = {

View File

@ -10,6 +10,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
@ -27,6 +28,7 @@ fun Loading(
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )

View File

@ -42,8 +42,6 @@ class ThermometerContract {
val ble: Ble.Thermometer val ble: Ble.Thermometer
) : Event() ) : Event()
data class OnTxChanged(val tx: Int) : Event()
object OnNavigateUpClicked : Event() object OnNavigateUpClicked : Event()
} }

View File

@ -201,6 +201,7 @@ fun ThermometerScreen(
when(state){ when(state){
is ThermometerContract.State.Display -> { is ThermometerContract.State.Display -> {
DisplayState( DisplayState(
origin = state.origin,
ble = state.thermometer, ble = state.thermometer,
onEvent = { onEvent = {
viewModel.setEvent(it) viewModel.setEvent(it)

View File

@ -23,7 +23,6 @@ class ThermometerViewModel @Inject constructor(
override fun handleEvents(event: ThermometerContract.Event) { override fun handleEvents(event: ThermometerContract.Event) {
when(event){ when(event){
is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, 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.OnBleChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
@ -46,18 +45,22 @@ class ThermometerViewModel @Inject constructor(
setEffect { ThermometerContract.Effect.Navigation.NavigateUp } setEffect { ThermometerContract.Effect.Navigation.NavigateUp }
} }
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnTxChanged
) {
}
private fun reduce( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnBleChanged event: ThermometerContract.Event.OnBleChanged
) { ) {
setState {
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( ThermometerContract.State.Display(
origin = event.ble, origin = event.ble,
thermometer = bleMapper.map(event.ble) as BleView.Thermometer, thermometer = bleMapper.map(event.ble) as BleView.Thermometer,
@ -66,6 +69,8 @@ class ThermometerViewModel @Inject constructor(
} }
} }
}
private fun reduce( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit event: ThermometerContract.Event.OnSaveIntervalEdit
@ -188,25 +193,48 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display){ 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 { viewModelScope.launch {
setState { setState {
state.copy( 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 = { onSuccess = {
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 { setState {
state.copy( currentState.copy(
origin = newBleObject,
thermometer = bleMapper.map(newBleObject) as BleView.Thermometer,
writeState = ThermometerContract.State.Display.WriteState.Success writeState = ThermometerContract.State.Display.WriteState.Success
) )
} }
}
}, },
onFailure = { onFailure = {
setState { setState {

View File

@ -19,10 +19,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
@Composable @Composable
fun DisplayState( fun DisplayState(
onEvent: (ThermometerContract.Event) -> Unit, onEvent: (ThermometerContract.Event) -> Unit,
origin: Ble.Thermometer,
ble: BleView.Thermometer ble: BleView.Thermometer
) { ) {
@ -40,7 +42,7 @@ fun DisplayState(
horizontal = 8.dp horizontal = 8.dp
) )
) { ) {
BleInfoView(bleInfo = ble.info) BleInfoView(bleInfo = origin.info)
} }
Column( Column(

View File

@ -1,15 +1,16 @@
package llc.arma.ble.app.ui.screen.thermometer.view package llc.arma.ble.app.ui.screen.thermometer.view
import androidx.compose.animation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material.icons.Icons
import androidx.compose.material3.Surface import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Text import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.model.BleView
import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract
@ -23,6 +24,22 @@ fun IntervalEdit(
mutableStateOf((state.thermometerState.historyInterval / 1000 / 60 / 60).toInt()) 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( Column(
modifier = Modifier modifier = Modifier
) { ) {
@ -35,28 +52,36 @@ fun IntervalEdit(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) { ) {
NumberPicker( NumberPicker(
dividersColor = MaterialTheme.colorScheme.primary, range = -1..maxDays,
value = value, value = dayValue,
onValueChange = { onValueChanged = { value = (it * 24) + hourValue }
value = it
},
textStyle = MaterialTheme.typography.titleMedium,
range = 1..100
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text( Text(text = "Дни")
text = "ч.",
style = MaterialTheme.typography.titleMedium 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)) Spacer(modifier = Modifier.height(16.dp))
@ -93,3 +118,84 @@ 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
)
}
}
}

View File

@ -278,6 +278,8 @@ class TemperatureHistoryViewModel @Inject constructor(
private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() { ) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() {
private var lastSerial: String? = null
override fun setInitialState() = TemperatureHistoryContract.State.Display( override fun setInitialState() = TemperatureHistoryContract.State.Display(
ProgressState.Indeterminate ProgressState.Indeterminate
) )
@ -297,7 +299,9 @@ class TemperatureHistoryViewModel @Inject constructor(
if(state is TemperatureHistoryContract.State.Display) { if(state is TemperatureHistoryContract.State.Display) {
if(state.loadingHistoryState is ProgressState.Indeterminate) { if(lastSerial != event.serial) {
lastSerial = event.serial
setState { setState {
TemperatureHistoryContract.State.Display(ProgressState.Indeterminate) TemperatureHistoryContract.State.Display(ProgressState.Indeterminate)
@ -323,6 +327,7 @@ class TemperatureHistoryViewModel @Inject constructor(
} }
} }
} }
private fun reduce( private fun reduce(

View File

@ -14,6 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -27,10 +28,8 @@ fun Write(
onEvent: (ThermometerContract.Event) -> Unit onEvent: (ThermometerContract.Event) -> Unit
) { ) {
val scope = rememberCoroutineScope()
Column( Column(
modifier = Modifier.animateContentSize { initialValue, targetValue -> } modifier = Modifier.animateContentSize()
) { ) {
Text( Text(
@ -44,6 +43,8 @@ fun Write(
when (state) { when (state) {
is ThermometerContract.State.Display.WriteState.DisplayPreview -> { is ThermometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) {
state.writeRequest.tx?.let { state.writeRequest.tx?.let {
Box( Box(
modifier = Modifier.padding( modifier = Modifier.padding(
@ -156,15 +157,15 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Surface( Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape, shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer, color = MaterialTheme.colorScheme.primaryContainer,
onClick = { onClick = {
onEvent(ThermometerContract.Event.OnWriteBle) onEvent(ThermometerContract.Event.OnWriteBle)
} },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -181,15 +182,15 @@ fun Write(
} }
Surface( Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape, shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.surfaceVariant,
onClick = { onClick = {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview) onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
} },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -205,6 +206,45 @@ fun Write(
} }
} else {
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 = "Ок"
)
}
}
}
} }
is ThermometerContract.State.Display.WriteState.Writing -> { is ThermometerContract.State.Display.WriteState.Writing -> {
@ -216,6 +256,7 @@ fun Write(
Spacer(modifier = Modifier.height(28.dp)) Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator( CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterHorizontally) .align(Alignment.CenterHorizontally)
) )

View File

@ -9,6 +9,7 @@ import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -183,18 +184,20 @@ class BleRepositoryImpl @Inject constructor(
} }
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun getBleBySerial( override suspend fun getBleBySerial(
serial: String serial: String
): Result<Ble, GetBleBySerial.GetBleException> = suspendCancellableCoroutine { ): Result<Flow<Ble>, BleException> {
deviceCache[serial]?.let { result -> 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( val state = Ble.BleState(
tx = when (result.scanRecord?.txPowerLevel) { tx = when (result.scanRecord?.txPowerLevel) {
@ -210,45 +213,104 @@ class BleRepositoryImpl @Inject constructor(
} }
) )
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
) )
)
}
delay(500)
}
}
)
}
BleInfo.Type.THERMOMETER -> { BleInfo.Type.THERMOMETER -> {
val thermometer = readThermometerState(result).fold( val tState = suspendCancellableCoroutine {
onFailure = { _ ->
return@launch it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) CoroutineScope(Dispatchers.IO).launch {
it.resume(readThermometerState(result))
}
}.fold(
onFailure = {
return Result.failure(it)
}, },
onSuccess = { return@fold 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( Ble.Thermometer(
info = info, info = newResult.info.copy(
rssi = if((SystemClock.elapsedRealtimeNanos() - newResult.timestampNanos) > 15_000_000_000) {
null
} else {
newResult.rssi
}
),
state = state, state = state,
thermometerState = thermometer thermometerState = tState
)
)
}
delay(500)
}
}
) )
} }
} }
it.resume(Result.success(resultValue)) {}
} }
} return llc.arma.ble.domain.Result.failure(BleException.UnexpectedResponse)
} else {
it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) {}
}
}
} }
@ -368,37 +430,73 @@ class BleRepositoryImpl @Inject constructor(
} }
} }
} }
override suspend fun writeBle( override suspend fun writeBle(
serial: String, serial: String,
request: Ble.Thermometer.WriteRequest request: Ble.Thermometer.WriteRequest
): Result<Unit, BleException> { ): Result<Unit, BleException> = suspendCancellableCoroutine {
deviceCache[serial]?.let { result -> deviceCache[serial]?.let { scanResult ->
request.tx?.let { writeTx(result.device, it) }?.onFailure { if(checkPermission()) {
return Result.failure(it)
}
request.historyInterval?.let { writeSaveInterval(result.device, it) }?.onFailure { var gatt: BluetoothGatt? = null
return Result.failure(it)
}
request.saveHistory?.let { writeSaveEnabled(result.device, it) }?.onFailure { val callback = WriteThermometerCallback(app, request) { result ->
return Result.failure(it)
}
writeToFlash(serial).onFailure { gatt?.close()
return Result.failure(it)
}
result.onSuccess {
deviceCache.remove(serial) deviceCache.remove(serial)
resultList.remove(serial) resultList.remove(serial)
}
it.resume(result)
} }
return Result.success(Unit) 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 {
Log.d("write", "in")
writeSaveInterval(result.device, it)
}?.onFailure {
Log.d("write", "in fail")
return Result.failure(it)
}
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)
}*/
}
} }
@ -510,7 +608,7 @@ class BleRepositoryImpl @Inject constructor(
serviceId = serviceUUID, serviceId = serviceUUID,
characteristicId = intervalWriteUUID, characteristicId = intervalWriteUUID,
writeData = mutableListOf<Byte>(3).apply { writeData = mutableListOf<Byte>(3).apply {
addAll(interval.toUInt().to4ByteArrayInBigEndian().toList()) addAll((interval / 1_000).toUInt().to4ByteArrayInBigEndian().toList())
}.toByteArray() }.toByteArray()
) )
@ -699,7 +797,9 @@ class BleRepositoryImpl @Inject constructor(
if (newState == BluetoothProfile.STATE_CONNECTED) { if (newState == BluetoothProfile.STATE_CONNECTED) {
if (checkPermission()) { if (checkPermission()) {
gatt.discoverServices() gatt.discoverServices()
} else { } else {
it.resume(Result.failure(BleException.PermissionDenied)) it.resume(Result.failure(BleException.PermissionDenied))
@ -708,7 +808,7 @@ class BleRepositoryImpl @Inject constructor(
} else { } else {
it.resume(Result.failure(BleException.UnexpectedResponse)) it.resume(Result.success(Unit))
bleGatt?.close() bleGatt?.close()
} }
@ -752,10 +852,14 @@ class BleRepositoryImpl @Inject constructor(
} }
Log.d("write", "service not found")
gatt.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse)) it.resume(Result.failure(BleException.UnexpectedResponse))
} else { } else {
gatt.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse)) it.resume(Result.failure(BleException.UnexpectedResponse))
} }
@ -787,6 +891,7 @@ class BleRepositoryImpl @Inject constructor(
} else { } else {
gatt.close()
it.resume(Result.failure(BleException.PermissionDenied)) it.resume(Result.failure(BleException.PermissionDenied))
} }

View File

@ -1,7 +1,6 @@
package llc.arma.ble.data package llc.arma.ble.data
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback 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.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import java.util.* import java.util.stream.Collectors
enum class Property { enum class Property {
DATA_SIZE, PACKAGE DATA_SIZE, PACKAGE
@ -24,12 +23,16 @@ class ReadHistoryCallback(
private val onResult: (Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>) -> Unit private val onResult: (Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() { ) : BluetoothGattCallback() {
private fun ByteArray.getUIntAt(idx: Int) = private fun ByteArray.get4byteUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or ((this[idx + 3].toUInt() and 0xFFu) shl 24) or
((this[idx + 2].toUInt() and 0xFFu) shl 16) or ((this[idx + 2].toUInt() and 0xFFu) shl 16) or
((this[idx + 1].toUInt() and 0xFFu) shl 8) or ((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu) (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 private var readProperty: Property? = null
init { init {
@ -122,6 +125,7 @@ class ReadHistoryCallback(
onCommonCharacteristicRead(gatt, characteristic, value, status) onCommonCharacteristicRead(gatt, characteristic, value, status)
} }
@OptIn(ExperimentalUnsignedTypes::class)
private fun onCommonCharacteristicRead( private fun onCommonCharacteristicRead(
gatt: BluetoothGatt, gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic, characteristic: BluetoothGattCharacteristic,
@ -161,30 +165,29 @@ class ReadHistoryCallback(
if(value[0] == 250.toByte()){ if(value[0] == 250.toByte()){
bleMeasureInterval = value.getUIntAt(2).toLong() bleMeasureInterval = value.get4byteUIntAt(4).toLong()
bleLastMeasureTime = value.getUIntAt(6).toLong() bleLastMeasureTime = value.get4byteUIntAt(8).toLong()
bleRealTime = value.getUIntAt(10).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( resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map { temperatureDataArray.chunked(2).map {
(it[0] + it[1] * 256).toFloat() / 100f (it[0] + it[1] * 256u).toFloat() / 100f
}.toMutableList() }.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 expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat()))) onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat()))) onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat())))
if(nextPackageDataCount != 0.toUByte()){ if(nextPackageDataCount != 0.toUInt()){
if (checkPermission()) { if (checkPermission()) {
@ -218,18 +221,18 @@ class ReadHistoryCallback(
if (value[0] == 251.toByte()) { if (value[0] == 251.toByte()) {
val nextPackageDataCount = value[1].toUByte() val nextPackageDataCount = value.get2byteUIntAt(2)
val temperatureDataArray = value.toList().subList(2, value.size) val temperatureDataArray = value.toUByteArray().toList().subList(4, value.size)
resultTemperaturePackage.addAll( resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map { 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()))) onResult(Result.success(ProgressState.Progress(expectedDataSize!!.toFloat() / resultTemperaturePackage.size.toFloat())))
if (nextPackageDataCount != 0.toUByte()) { if (nextPackageDataCount != 0.toUInt()) {
val writeData = byteArrayOf(5) val writeData = byteArrayOf(5)

View File

@ -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, BleException>) -> 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<UUID, ByteArray>? = null
uuid = request.historyInterval?.let {
this.request = request.copy(
historyInterval = null
)
Pair(
intervalWriteUUID,
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInBigEndian().toList())
}.toByteArray()
)
}
uuid = request.saveHistory?.let {
this.request = request.copy(
saveHistory = null
)
Pair(
saveEnabledWriteUUID,
mutableListOf<Byte>(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<Unit, BleException> {
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
}
}
}

View File

@ -26,13 +26,13 @@ sealed class Ble(
val value: Float val value: Float
) )
class ThermometerState( data class ThermometerState(
val temperature: Float, val temperature: Float,
val saveHistory: Boolean, val saveHistory: Boolean,
val historyInterval: Long val historyInterval: Long
) )
class WriteRequest( data class WriteRequest(
val tx: BleState.TX?, val tx: BleState.TX?,
val saveHistory: Boolean?, val saveHistory: Boolean?,
val historyInterval: Long? val historyInterval: Long?
@ -40,7 +40,7 @@ sealed class Ble(
} }
class BleState( data class BleState(
val tx: TX val tx: TX
){ ){

View File

@ -2,16 +2,16 @@ package llc.arma.ble.domain.model
import java.util.UUID import java.util.UUID
class BleInfo( data class BleInfo(
val name: String, val name: String,
val serial: String, val serial: String,
val batteryLevel: Int, val batteryLevel: Int,
val rssi: Int, val rssi: Int?,
val type: Type val type: Type
){ ){
enum class Type(val serviceUUID: String?) { enum class Type {
BEACON(null), THERMOMETER("a77db03a-9bc4-11ed-a8fc-0242ac120002") BEACON, THERMOMETER
} }
} }

View File

@ -15,7 +15,7 @@ interface BleRepository {
fun getBleAroundFlow(): Flow<Result<List<BleInfo>, BleException>> fun getBleAroundFlow(): Flow<Result<List<BleInfo>, BleException>>
suspend fun getBleBySerial(serial: String): Result<Ble, GetBleBySerial.GetBleException> suspend fun getBleBySerial(serial: String) : Result<Flow<Ble>, BleException>
suspend fun getTemperatureHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> suspend fun getTemperatureHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>>

View File

@ -1,15 +1,17 @@
package llc.arma.ble.domain.usecase package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject import javax.inject.Inject
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
class GetBleBySerial @Inject constructor( class GetBleBySerial @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository
) { ) {
suspend operator fun invoke(serial: String): Result<Ble, GetBleException> = suspend operator fun invoke(serial: String): Result<Flow<Ble>, BleException> =
bleRepository.getBleBySerial(serial) bleRepository.getBleBySerial(serial)
sealed class GetBleException { sealed class GetBleException {