diff --git a/.idea/misc.xml b/.idea/misc.xml index 773fe0f..4c5e777 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt index bc942a0..d86c23c 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt @@ -11,19 +11,55 @@ class BleListContract { sealed class Event : ViewEvent { + object OnResetFilter : Event() + + object OnHideFilter : Event() + + object OnShowFilter : Event() + data class OnConnectToBle( val bleAddress: String ) : Event() + data class OnRssiRangeChanged( + val rssi: ClosedFloatingPointRange + ) : Event() + + data class OnMacFilterChanged( + val mac: String + ) : Event() + + data class OnNameFilterChanged( + val name: String + ) : Event() + + data class OnTypeChanged( + val type: BleInfo.Type? + ) : Event() + } data class State( val connectedBleList: List, - val bleList: List - ) : ViewState + val bleList: List, + val filter: Filter + ) : ViewState { + + data class Filter( + val name: String = "", + val mac: String = "", + val rssi: ClosedFloatingPointRange = (-100f)..(-30f), + val bleType: BleInfo.Type? = null + ) + + } sealed class Effect : ViewSideEffect { + object ShowFilter : Effect() + + object HideFilter : Effect() + sealed class Navigation : Effect() { data class NavigateToBle( diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt index e6e2462..ea79261 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt @@ -1,5 +1,6 @@ package llc.arma.ble.app.ui.screen.ble +import android.os.SystemClock import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -7,11 +8,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ContentAlpha import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -19,8 +25,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import llc.arma.ble.app.ui.common.rememberBottomDialogState import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.ConnectedBleInfo @@ -33,19 +42,80 @@ fun BleListScreen( val viewModel = hiltViewModel() val state = viewModel.viewState.value + val bottomDialog = rememberBottomDialogState() + LaunchedEffect("effect"){ viewModel.effect.onEach { when(it){ is BleListContract.Effect.Navigation -> onNavigationEvent(it) + is BleListContract.Effect.HideFilter -> launch { + bottomDialog.hide() + } + is BleListContract.Effect.ShowFilter -> launch { + bottomDialog.show { + Filter( + filter = viewModel.viewState.value.filter, + onEvent = { + viewModel.setEvent(it) + } + ) + } + } } }.launchIn(this) } Column { - CenterAlignedTopAppBar( + TopAppBar( title = { Text(text = "Arma BLE") + }, + actions = { + + Row( + modifier = Modifier + .padding(horizontal = 8.dp) + .align(Alignment.CenterVertically) + ) { + + Text(text = "${state.bleList.size}") + + Spacer(modifier = Modifier.width(12.dp)) + + Text(text = "${state.bleList.filter { + it.batteryLevel == 100 + }.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}") + + Text(text = " | ") + + Text( + text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}", + color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled) + ) + + Text(text = " | ") + + Text( + text = "${state.bleList.filter { it.batteryLevel < 100 }.size}", + color = MaterialTheme.colorScheme.error + ) + + } + + IconButton( + onClick = { + viewModel.setEvent(BleListContract.Event.OnShowFilter) + } + ) { + + Icon( + imageVector = Icons.Rounded.FilterAlt, + contentDescription = null + ) + + } + } ) @@ -71,7 +141,14 @@ fun BleListScreen( } - items(items = state.bleList) { + val filteredData = state.bleList.filter { + (it.type == state.filter.bleType || state.filter.bleType == null) && + it.name.contains(state.filter.name) && + it.serial.contains(state.filter.mac) && + state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) + } + + items(items = filteredData.sortedBy { it.name }.reversed()) { BleItem( ble = it, @@ -113,14 +190,48 @@ private fun BleItem( onClick: () -> Unit ){ + val color = if(ble.batteryLevel < 100){ + MaterialTheme.colorScheme.errorContainer + } else { + MaterialTheme.colorScheme.background + } + + val highAlpha = ContentAlpha.high + val disabledAlpha = ContentAlpha.disabled + + var alpha by remember { + mutableStateOf( + if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){ + disabledAlpha + } else { + highAlpha + } + ) + } + + LaunchedEffect(ble.scanTime) { + while(true) { + alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){ + disabledAlpha + } else { + highAlpha + } + + delay(800) + } + } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) + .background(color) .clickable { onClick() } .padding(vertical = 8.dp, horizontal = 16.dp) + .alpha(alpha) + ) { ItemIcon { @@ -158,10 +269,20 @@ private fun BleItem( contentDescription = null ) - Text( - style = MaterialTheme.typography.bodyMedium, - text = ble.rssi.toString() + " dBm" - ) + Box { + + Text( + style = MaterialTheme.typography.bodyMedium, + text = "-999 dBm", + modifier = Modifier.alpha(0f) + ) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ble.rssi.toString() + " dBm" + ) + + } } @@ -170,19 +291,87 @@ private fun BleItem( modifier = Modifier.alpha(0.7f) ) { + val color = if(ble.batteryLevel < 100){ + MaterialTheme.colorScheme.error + } else { + LocalContentColor.current + } + Icon( modifier = Modifier.size(16.dp), imageVector = Icons.Rounded.BatteryFull, - contentDescription = null + contentDescription = null, + tint = color ) - Text( - style = MaterialTheme.typography.bodyMedium, - text = ble.batteryLevel.toString() + " %" - ) + Box { + + Text( + style = MaterialTheme.typography.bodyMedium, + text = "100 %", + modifier = Modifier.alpha(0f) + ) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ble.batteryLevel.toString() + " %", + color = color + ) + + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.alpha(0.7f) + ) { + + val color = if(ble.batteryLevel < 100){ + MaterialTheme.colorScheme.error + } else { + LocalContentColor.current + } + + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Rounded.ArrowRightAlt, + contentDescription = null, + tint = color + ) + + Box { + + Text( + style = MaterialTheme.typography.bodyMedium, + text = "99s", + modifier = Modifier.alpha(0f) + ) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ">1m", + modifier = Modifier.alpha(0f) + ) + + val lastAdv = ((SystemClock.elapsedRealtime() - ble.scanTime) / 1_000) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = if(lastAdv > 60){ + ">1m" + } else { + "${lastAdv}s" + } + ) + + } + + + } + + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt index 5332fd7..19a7af9 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt @@ -23,7 +23,7 @@ class BleListViewModel @Inject constructor( it.fold( onSuccess = { setState { - BleListContract.State( + copy( connectedBleList = emptyList(), bleList = it ) @@ -38,11 +38,18 @@ class BleListViewModel @Inject constructor( } - override fun setInitialState(): BleListContract.State = BleListContract.State(emptyList(), emptyList()) + override fun setInitialState(): BleListContract.State = BleListContract.State(emptyList(), emptyList(), BleListContract.State.Filter()) override fun handleEvents(event: BleListContract.Event) { when(event){ is BleListContract.Event.OnConnectToBle -> reduce(viewState.value, event) + is BleListContract.Event.OnHideFilter -> reduce(viewState.value, event) + is BleListContract.Event.OnMacFilterChanged -> reduce(viewState.value, event) + is BleListContract.Event.OnNameFilterChanged -> reduce(viewState.value, event) + is BleListContract.Event.OnResetFilter -> reduce(viewState.value, event) + is BleListContract.Event.OnRssiRangeChanged -> reduce(viewState.value, event) + is BleListContract.Event.OnShowFilter -> reduce(viewState.value, event) + is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event) } } @@ -55,4 +62,83 @@ class BleListViewModel @Inject constructor( } } + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnHideFilter + ) { + setEffect { + BleListContract.Effect.HideFilter + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnMacFilterChanged + ) { + setState { + copy( + filter = this.filter.copy(mac = event.mac) + ) + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnNameFilterChanged + ) { + setState { + copy( + filter = this.filter.copy(name = event.name) + ) + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnResetFilter + ) { + + setState { + copy( + filter = BleListContract.State.Filter() + ) + } + + setEffect { + BleListContract.Effect.HideFilter + } + + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnRssiRangeChanged + ) { + setState { + copy( + filter = this.filter.copy(rssi = event.rssi) + ) + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnTypeChanged + ) { + setState { + copy( + filter = this.filter.copy(bleType = event.type) + ) + } + } + + private fun reduce( + state: BleListContract.State, + event: BleListContract.Event.OnShowFilter + ) { + setEffect { + BleListContract.Effect.ShowFilter + } + } + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt new file mode 100644 index 0000000..30e8927 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt @@ -0,0 +1,337 @@ +package llc.arma.ble.app.ui.screen.ble + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Bluetooth +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.ShortText +import androidx.compose.material.icons.rounded.SignalCellularAlt +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import llc.arma.ble.domain.model.BleInfo + +private val BleInfo.Type?.localized: String + get() { + return when(this){ + BleInfo.Type.BEACON -> "Маяк" + BleInfo.Type.THERMOMETER -> "Термодатчик" + null -> "Все" + } + } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Filter( + filter: BleListContract.State.Filter, + onEvent: (BleListContract.Event) -> Unit +) { + + Column( + + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Фильтр", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.padding(horizontal = 12.dp) + ) { + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + Icon( + imageVector = Icons.Rounded.Bluetooth, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = Modifier.fillMaxWidth().padding(end = 8.dp), + expanded = expanded, + onExpandedChange = { + expanded = it + } + ) { + + OutlinedTextField( + modifier = Modifier.menuAnchor().fillMaxWidth(), + readOnly = true, + value = filter.bleType.localized, + onValueChange = { }, + label = { Text("Тип") }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon( + expanded = expanded + ) + } + ) + + ExposedDropdownMenu( + modifier = Modifier.background(MaterialTheme.colorScheme.background).fillMaxWidth(), + expanded = expanded, + onDismissRequest = { + expanded = false + } + ) { + + mutableListOf(null).apply { + addAll(BleInfo.Type.values()) + }.forEach { selectionOption -> + DropdownMenuItem( + onClick = { + onEvent( + BleListContract.Event.OnTypeChanged( + selectionOption + ) + ) + expanded = false + }, + text = { + Text(text = selectionOption.localized) + } + ) + } + } + } + + } + + Row( + verticalAlignment = Alignment.CenterVertically + ){ + + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + OutlinedTextField( + value = filter.name, + singleLine = true, + onValueChange = { + onEvent(BleListContract.Event.OnNameFilterChanged(it)) + }, + label = { + Text(text = "Имя") + }, + trailingIcon = { + + if(filter.name.isNotEmpty()) { + + IconButton( + onClick = { onEvent(BleListContract.Event.OnNameFilterChanged("")) } + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + + } + + }, + modifier = Modifier + .padding(end = 8.dp) + .fillMaxWidth() + + ) + + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + + Icon( + imageVector = Icons.Rounded.ShortText, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + OutlinedTextField( + value = filter.mac, + singleLine = true, + onValueChange = { + onEvent(BleListContract.Event.OnMacFilterChanged(it)) + }, + label = { + Text(text = "Mac") + }, + trailingIcon = { + + if (filter.mac.isNotEmpty()) { + + IconButton( + onClick = { onEvent(BleListContract.Event.OnMacFilterChanged("")) } + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = null + ) + } + + } + + }, + modifier = Modifier + .padding(end = 8.dp) + .fillMaxWidth() + ) + + } + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(end = 8.dp) + ) { + + Icon( + imageVector = Icons.Rounded.SignalCellularAlt, + contentDescription = null, + modifier = Modifier.padding(12.dp) + ) + + Column() { + + RangeSlider( + value = filter.rssi, + onValueChange = { + onEvent(BleListContract.Event.OnRssiRangeChanged(it)) + }, + valueRange = (-100f)..(-30f), + steps = 69, + colors = SliderDefaults.colors( + activeTickColor = MaterialTheme.colorScheme.primary, + inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f) + ) + ) + + Row() { + + Text(text = filter.rssi.start.toInt().toString() + " dBm") + + Spacer(modifier = Modifier.weight(1f)) + + Text(text = filter.rssi.endInclusive.toInt().toString() + " dBm") + + } + + } + + } + + Spacer(modifier = Modifier.height(20.dp)) + + Box( + modifier = Modifier + ) { + + Surface( + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent(BleListContract.Event.OnHideFilter) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Применить" + ) + + } + + } + + } + + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + ) { + + Surface( + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.secondaryContainer, + onClick = { + onEvent(BleListContract.Event.OnResetFilter) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = MaterialTheme.typography.labelLarge, + text = "Сбросить" + ) + + } + + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + } + + } + +} \ No newline at end of file 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 de1e5f0..9e84880 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 @@ -185,10 +185,14 @@ fun DisplayState( Text( text = "Интервал измерений" ) + + val hours = ble.thermometerState.historyInterval / 1000 / 60 / 60 + val minutes = (ble.thermometerState.historyInterval - ( hours * 1000 * 60 * 60 )) / 1000 / 60 + Text( color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.bodyMedium, - text = "${ble.thermometerState.historyInterval / 1000 / 60 / 60} ч." + text = "$hours ч. $minutes мин." ) } 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 539ab11..c7c8086 100644 --- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt +++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt @@ -4,7 +4,6 @@ import android.Manifest import android.app.Application import android.bluetooth.* import android.bluetooth.le.ScanCallback -import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.pm.PackageManager @@ -24,13 +23,10 @@ import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.ConnectedBleInfo import llc.arma.ble.domain.repository.BleRepository -import llc.arma.ble.domain.usecase.GetBleBySerial -import java.nio.charset.Charset import java.util.* import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002") @@ -43,6 +39,19 @@ val passwordWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120 val txWriteUUID: UUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb") val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002") +@OptIn(ExperimentalUnsignedTypes::class) +fun UByteArray.toTemperature(): Float { + val uShort = (this[0] + this[1] * 256u).toUShort() + + val result = if (uShort > Short.MAX_VALUE.toUShort()) { + ((uShort.inv() + 1u).toFloat().unaryMinus()) / 100f + } else { + uShort.toFloat() / 100f + } + + return result +} + @Singleton class BleRepositoryImpl @Inject constructor( private val app: Application @@ -55,7 +64,8 @@ class BleRepositoryImpl @Inject constructor( serial = device.address, batteryLevel = batteryLevel ?: 0, rssi = rssi, - type = type + type = type, + scanTime = timestampNanos / 1_000_000 ) } @@ -85,7 +95,7 @@ class BleRepositoryImpl @Inject constructor( (this[idx].toUInt() and 0xFFu) private val deviceCache = mutableMapOf() - val resultList = mutableMapOf() + val resultList: MutableMap = Collections.synchronizedMap(mutableMapOf()) override fun getConnectedBle(): List { if(checkPermission()) { @@ -143,6 +153,32 @@ class BleRepositoryImpl @Inject constructor( } + override fun onBatchScanResults(results: MutableList) { + super.onBatchScanResults(results) + + if (checkPermission()) { + + results.forEach { result -> + + if (result.scanRecord?.deviceName?.contains("ArmA") == true) { + + resultList[result.device.address] = result.info + + deviceCache[result.device.address] = result + + } + + } + + } else { + CoroutineScope(Dispatchers.IO).launch { + send( + Result.failure(BleException.PermissionDenied) + ) + } + } + + } } val bleScanner = @@ -342,10 +378,22 @@ class BleRepositoryImpl @Inject constructor( } + @OptIn(ExperimentalUnsignedTypes::class) private suspend fun readTemperature( record: ScanResult ): Result { + writeCharacteristic( + device = record.device, + serviceId = serviceUUID, + characteristicId = temperatureReadUUID, + writeData = byteArrayOf(1, 1) + ).onFailure { + return Result.failure(it) + } + + delay(2_000) + val dataResult = readCharacteristic( device = record.device, serviceId = serviceUUID, @@ -357,7 +405,7 @@ class BleRepositoryImpl @Inject constructor( onSuccess = { return@fold it } ) - return Result.success((dataResult[0] + dataResult[1] * 256).toFloat() / 100f) + return Result.success(dataResult.toUByteArray().toTemperature()) } @@ -701,21 +749,22 @@ class BleRepositoryImpl @Inject constructor( } else { + bleGatt?.close() it.resume(Result.failure(BleException.PermissionDenied)) } } else { - it.resume(Result.success(Unit)) bleGatt?.close() + it.resume(Result.success(Unit)) } } else { - it.resume(Result.failure(BleException.UnexpectedResponse)) bleGatt?.close() + it.resume(Result.failure(BleException.UnexpectedResponse)) } @@ -743,6 +792,7 @@ class BleRepositoryImpl @Inject constructor( } else { + bleGatt?.close() it.resume(Result.failure(BleException.PermissionDenied)) } @@ -753,12 +803,12 @@ class BleRepositoryImpl @Inject constructor( Log.d("write", "service not found") - gatt.disconnect() + bleGatt?.close() it.resume(Result.failure(BleException.UnexpectedResponse)) } else { - gatt.disconnect() + bleGatt?.close() it.resume(Result.failure(BleException.UnexpectedResponse)) } 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 ce51983..a8fcb0a 100644 --- a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt @@ -7,6 +7,7 @@ import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic 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 @@ -132,6 +133,8 @@ class ReadHistoryCallback( value: ByteArray, status: Int ){ + Log.d("read", value[0].toString()) + if(status == BluetoothGatt.GATT_SUCCESS){ when(readProperty){ Property.DATA_SIZE -> { @@ -175,17 +178,15 @@ class ReadHistoryCallback( resultTemperaturePackage.addAll( temperatureDataArray.chunked(2).map { - (it[0] + it[1] * 256u).toFloat() / 100f + it.toUByteArray().toTemperature() }.toMutableList() ) - val totalDataSize = value.get2byteUIntAt(2).toInt() + temperatureDataArray.size / 2 - 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()))) + Log.d("read", expectedDataSize.toString()) + onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat()))) + onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize!!.toFloat()))) if(nextPackageDataCount != 0.toUInt()){ @@ -226,11 +227,11 @@ class ReadHistoryCallback( resultTemperaturePackage.addAll( temperatureDataArray.chunked(2).map { - (it[0] + it[1] * 256u).toFloat() / 100f + it.toUByteArray().toTemperature() } ) - onResult(Result.success(ProgressState.Progress(expectedDataSize!!.toFloat() / resultTemperaturePackage.size.toFloat()))) + onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize!!.toFloat()))) if (nextPackageDataCount != 0.toUInt()) { diff --git a/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt index a2b81e2..d12574b 100644 --- a/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt @@ -10,6 +10,10 @@ import android.content.pm.PackageManager import android.os.Build import android.util.Log import androidx.core.app.ActivityCompat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import llc.arma.ble.domain.Result import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.model.Ble @@ -69,10 +73,10 @@ class WriteThermometerCallback( if(request.tx != null || request.saveHistory != null || request.historyInterval != null) { - fun UInt.to4ByteArrayInBigEndian(): ByteArray = + fun UInt.to4ByteArrayInLittleEndian(): ByteArray = (3 downTo 0).map { (this shr (it * Byte.SIZE_BITS)).toByte() - }.reversed().toByteArray() + }.toByteArray() var uuid: Pair? = null @@ -85,7 +89,7 @@ class WriteThermometerCallback( Pair( intervalWriteUUID, mutableListOf(3).apply { - addAll((it).toUInt().to4ByteArrayInBigEndian().toList()) + addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()) }.toByteArray() ) } @@ -203,18 +207,20 @@ class WriteThermometerCallback( } - fun BluetoothGatt.writeCharacteristic( + private fun BluetoothGatt.writeCharacteristic( characteristic: BluetoothGattCharacteristic, data: ByteArray ): Result { return if(checkPermission()){ + Log.d("write", data.asUByteArray().joinToString(" ") { it.toString(16).padStart(2, '0') }) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) }else{ - characteristic.writeType + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT characteristic.value = data writeCharacteristic(characteristic) } @@ -227,7 +233,7 @@ class WriteThermometerCallback( } - fun checkPermission(): Boolean { + private fun checkPermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) == 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 6a72a27..ac85156 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 @@ -1,13 +1,12 @@ package llc.arma.ble.domain.model -import java.util.UUID - data class BleInfo( val name: String, val serial: String, val batteryLevel: Int, val rssi: Int?, - val type: Type + val type: Type, + val scanTime: Long ){ enum class Type {