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.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"

View File

@ -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 "Нет сигнала"
)
}
}

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.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(

View File

@ -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 = {

View File

@ -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)
)

View File

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

View File

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

View File

@ -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 {

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.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(

View File

@ -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))
@ -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
) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() {
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(

View File

@ -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)
)

View File

@ -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<Ble, GetBleBySerial.GetBleException> = suspendCancellableCoroutine {
): Result<Flow<Ble>, 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<Unit, BleException> {
): Result<Unit, BleException> = 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<Byte>(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))
}

View File

@ -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<ProgressState<List<Ble.Thermometer.MeasurePoint>>, 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)

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
)
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
){

View File

@ -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
}
}

View File

@ -15,7 +15,7 @@ interface BleRepository {
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>>

View File

@ -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<Ble, GetBleException> =
suspend operator fun invoke(serial: String): Result<Flow<Ble>, BleException> =
bleRepository.getBleBySerial(serial)
sealed class GetBleException {