Merge branch 'master' of https://github.com/Vineyro/Ble
Conflicts: .idea/misc.xml
This commit is contained in:
commit
e8853d8cda
|
|
@ -1,6 +1,6 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="temurin-11" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,55 @@ class BleListContract {
|
||||||
|
|
||||||
sealed class Event : ViewEvent {
|
sealed class Event : ViewEvent {
|
||||||
|
|
||||||
|
object OnResetFilter : Event()
|
||||||
|
|
||||||
|
object OnHideFilter : Event()
|
||||||
|
|
||||||
|
object OnShowFilter : Event()
|
||||||
|
|
||||||
data class OnConnectToBle(
|
data class OnConnectToBle(
|
||||||
val bleAddress: String
|
val bleAddress: String
|
||||||
) : Event()
|
) : Event()
|
||||||
|
|
||||||
|
data class OnRssiRangeChanged(
|
||||||
|
val rssi: ClosedFloatingPointRange<Float>
|
||||||
|
) : 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(
|
data class State(
|
||||||
val connectedBleList: List<ConnectedBleInfo>,
|
val connectedBleList: List<ConnectedBleInfo>,
|
||||||
val bleList: List<BleInfo>
|
val bleList: List<BleInfo>,
|
||||||
) : ViewState
|
val filter: Filter
|
||||||
|
) : ViewState {
|
||||||
|
|
||||||
|
data class Filter(
|
||||||
|
val name: String = "",
|
||||||
|
val mac: String = "",
|
||||||
|
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-30f),
|
||||||
|
val bleType: BleInfo.Type? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Effect : ViewSideEffect {
|
sealed class Effect : ViewSideEffect {
|
||||||
|
|
||||||
|
object ShowFilter : Effect()
|
||||||
|
|
||||||
|
object HideFilter : Effect()
|
||||||
|
|
||||||
sealed class Navigation : Effect() {
|
sealed class Navigation : Effect() {
|
||||||
|
|
||||||
data class NavigateToBle(
|
data class NavigateToBle(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package llc.arma.ble.app.ui.screen.ble
|
package llc.arma.ble.app.ui.screen.ble
|
||||||
|
|
||||||
|
import android.os.SystemClock
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
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.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.*
|
import androidx.compose.material.icons.rounded.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
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.graphics.StrokeCap
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
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.BleInfo
|
||||||
import llc.arma.ble.domain.model.ConnectedBleInfo
|
import llc.arma.ble.domain.model.ConnectedBleInfo
|
||||||
|
|
||||||
|
|
@ -33,19 +42,80 @@ fun BleListScreen(
|
||||||
val viewModel = hiltViewModel<BleListViewModel>()
|
val viewModel = hiltViewModel<BleListViewModel>()
|
||||||
val state = viewModel.viewState.value
|
val state = viewModel.viewState.value
|
||||||
|
|
||||||
|
val bottomDialog = rememberBottomDialogState()
|
||||||
|
|
||||||
LaunchedEffect("effect"){
|
LaunchedEffect("effect"){
|
||||||
viewModel.effect.onEach {
|
viewModel.effect.onEach {
|
||||||
when(it){
|
when(it){
|
||||||
is BleListContract.Effect.Navigation -> onNavigationEvent(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)
|
}.launchIn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(text = "Arma BLE")
|
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(
|
BleItem(
|
||||||
ble = it,
|
ble = it,
|
||||||
|
|
@ -113,14 +190,48 @@ private fun BleItem(
|
||||||
onClick: () -> Unit
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(color)
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
.alpha(alpha)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
ItemIcon {
|
ItemIcon {
|
||||||
|
|
@ -158,6 +269,14 @@ private fun BleItem(
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Box {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
text = "-999 dBm",
|
||||||
|
modifier = Modifier.alpha(0f)
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
text = ble.rssi.toString() + " dBm"
|
text = ble.rssi.toString() + " dBm"
|
||||||
|
|
@ -165,24 +284,94 @@ private fun BleItem(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.alpha(0.7f)
|
modifier = Modifier.alpha(0.7f)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val color = if(ble.batteryLevel < 100){
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
} else {
|
||||||
|
LocalContentColor.current
|
||||||
|
}
|
||||||
|
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
imageVector = Icons.Rounded.BatteryFull,
|
imageVector = Icons.Rounded.BatteryFull,
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
tint = color
|
||||||
|
)
|
||||||
|
|
||||||
|
Box {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
text = "100 %",
|
||||||
|
modifier = Modifier.alpha(0f)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
text = ble.batteryLevel.toString() + " %"
|
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"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class BleListViewModel @Inject constructor(
|
||||||
it.fold(
|
it.fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
setState {
|
setState {
|
||||||
BleListContract.State(
|
copy(
|
||||||
connectedBleList = emptyList(),
|
connectedBleList = emptyList(),
|
||||||
bleList = it
|
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) {
|
override fun handleEvents(event: BleListContract.Event) {
|
||||||
when(event){
|
when(event){
|
||||||
is BleListContract.Event.OnConnectToBle -> reduce(viewState.value, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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<BleInfo.Type?>(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))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -185,10 +185,14 @@ fun DisplayState(
|
||||||
Text(
|
Text(
|
||||||
text = "Интервал измерений"
|
text = "Интервал измерений"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val hours = ble.thermometerState.historyInterval / 1000 / 60 / 60
|
||||||
|
val minutes = (ble.thermometerState.historyInterval - ( hours * 1000 * 60 * 60 )) / 1000 / 60
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
text = "${ble.thermometerState.historyInterval / 1000 / 60 / 60} ч."
|
text = "$hours ч. $minutes мин."
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.Manifest
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.bluetooth.*
|
import android.bluetooth.*
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanFilter
|
|
||||||
import android.bluetooth.le.ScanResult
|
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
|
||||||
|
|
@ -24,13 +23,10 @@ import llc.arma.ble.domain.model.Ble
|
||||||
import llc.arma.ble.domain.model.BleInfo
|
import llc.arma.ble.domain.model.BleInfo
|
||||||
import llc.arma.ble.domain.model.ConnectedBleInfo
|
import llc.arma.ble.domain.model.ConnectedBleInfo
|
||||||
import llc.arma.ble.domain.repository.BleRepository
|
import llc.arma.ble.domain.repository.BleRepository
|
||||||
import llc.arma.ble.domain.usecase.GetBleBySerial
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
|
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 txWriteUUID: UUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb")
|
||||||
val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
|
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
|
@Singleton
|
||||||
class BleRepositoryImpl @Inject constructor(
|
class BleRepositoryImpl @Inject constructor(
|
||||||
private val app: Application
|
private val app: Application
|
||||||
|
|
@ -55,7 +64,8 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
serial = device.address,
|
serial = device.address,
|
||||||
batteryLevel = batteryLevel ?: 0,
|
batteryLevel = batteryLevel ?: 0,
|
||||||
rssi = rssi,
|
rssi = rssi,
|
||||||
type = type
|
type = type,
|
||||||
|
scanTime = timestampNanos / 1_000_000
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +95,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
(this[idx].toUInt() and 0xFFu)
|
(this[idx].toUInt() and 0xFFu)
|
||||||
|
|
||||||
private val deviceCache = mutableMapOf<String, ScanResult>()
|
private val deviceCache = mutableMapOf<String, ScanResult>()
|
||||||
val resultList = mutableMapOf<String, BleInfo>()
|
val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())
|
||||||
override fun getConnectedBle(): List<ConnectedBleInfo> {
|
override fun getConnectedBle(): List<ConnectedBleInfo> {
|
||||||
|
|
||||||
if(checkPermission()) {
|
if(checkPermission()) {
|
||||||
|
|
@ -143,6 +153,32 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
|
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 =
|
val bleScanner =
|
||||||
|
|
@ -342,10 +378,22 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUnsignedTypes::class)
|
||||||
private suspend fun readTemperature(
|
private suspend fun readTemperature(
|
||||||
record: ScanResult
|
record: ScanResult
|
||||||
): Result<Float, BleException> {
|
): Result<Float, BleException> {
|
||||||
|
|
||||||
|
writeCharacteristic(
|
||||||
|
device = record.device,
|
||||||
|
serviceId = serviceUUID,
|
||||||
|
characteristicId = temperatureReadUUID,
|
||||||
|
writeData = byteArrayOf(1, 1)
|
||||||
|
).onFailure {
|
||||||
|
return Result.failure(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(2_000)
|
||||||
|
|
||||||
val dataResult = readCharacteristic(
|
val dataResult = readCharacteristic(
|
||||||
device = record.device,
|
device = record.device,
|
||||||
serviceId = serviceUUID,
|
serviceId = serviceUUID,
|
||||||
|
|
@ -357,7 +405,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
onSuccess = { return@fold it }
|
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 {
|
} else {
|
||||||
|
|
||||||
|
bleGatt?.close()
|
||||||
it.resume(Result.failure(BleException.PermissionDenied))
|
it.resume(Result.failure(BleException.PermissionDenied))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
it.resume(Result.success(Unit))
|
|
||||||
bleGatt?.close()
|
bleGatt?.close()
|
||||||
|
it.resume(Result.success(Unit))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
it.resume(Result.failure(BleException.UnexpectedResponse))
|
|
||||||
bleGatt?.close()
|
bleGatt?.close()
|
||||||
|
it.resume(Result.failure(BleException.UnexpectedResponse))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -743,6 +792,7 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
bleGatt?.close()
|
||||||
it.resume(Result.failure(BleException.PermissionDenied))
|
it.resume(Result.failure(BleException.PermissionDenied))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -753,12 +803,12 @@ class BleRepositoryImpl @Inject constructor(
|
||||||
|
|
||||||
Log.d("write", "service not found")
|
Log.d("write", "service not found")
|
||||||
|
|
||||||
gatt.disconnect()
|
bleGatt?.close()
|
||||||
it.resume(Result.failure(BleException.UnexpectedResponse))
|
it.resume(Result.failure(BleException.UnexpectedResponse))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
gatt.disconnect()
|
bleGatt?.close()
|
||||||
it.resume(Result.failure(BleException.UnexpectedResponse))
|
it.resume(Result.failure(BleException.UnexpectedResponse))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.bluetooth.BluetoothGattCallback
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import llc.arma.ble.domain.Result
|
import llc.arma.ble.domain.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
|
|
@ -132,6 +133,8 @@ class ReadHistoryCallback(
|
||||||
value: ByteArray,
|
value: ByteArray,
|
||||||
status: Int
|
status: Int
|
||||||
){
|
){
|
||||||
|
Log.d("read", value[0].toString())
|
||||||
|
|
||||||
if(status == BluetoothGatt.GATT_SUCCESS){
|
if(status == BluetoothGatt.GATT_SUCCESS){
|
||||||
when(readProperty){
|
when(readProperty){
|
||||||
Property.DATA_SIZE -> {
|
Property.DATA_SIZE -> {
|
||||||
|
|
@ -175,17 +178,15 @@ class ReadHistoryCallback(
|
||||||
|
|
||||||
resultTemperaturePackage.addAll(
|
resultTemperaturePackage.addAll(
|
||||||
temperatureDataArray.chunked(2).map {
|
temperatureDataArray.chunked(2).map {
|
||||||
(it[0] + it[1] * 256u).toFloat() / 100f
|
it.toUByteArray().toTemperature()
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
)
|
)
|
||||||
|
|
||||||
val totalDataSize = value.get2byteUIntAt(2).toInt() + temperatureDataArray.size / 2
|
|
||||||
|
|
||||||
val nextPackageDataCount = value.get2byteUIntAt(2)
|
val nextPackageDataCount = value.get2byteUIntAt(2)
|
||||||
expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
|
expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
|
||||||
|
Log.d("read", expectedDataSize.toString())
|
||||||
onResult(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
|
onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat())))
|
||||||
onResult(Result.success(ProgressState.Progress(nextPackageDataCount.toFloat() / totalDataSize.toFloat())))
|
onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize!!.toFloat())))
|
||||||
|
|
||||||
if(nextPackageDataCount != 0.toUInt()){
|
if(nextPackageDataCount != 0.toUInt()){
|
||||||
|
|
||||||
|
|
@ -226,11 +227,11 @@ class ReadHistoryCallback(
|
||||||
|
|
||||||
resultTemperaturePackage.addAll(
|
resultTemperaturePackage.addAll(
|
||||||
temperatureDataArray.chunked(2).map {
|
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()) {
|
if (nextPackageDataCount != 0.toUInt()) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.ActivityCompat
|
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.Result
|
||||||
import llc.arma.ble.domain.common.BleException
|
import llc.arma.ble.domain.common.BleException
|
||||||
import llc.arma.ble.domain.model.Ble
|
import llc.arma.ble.domain.model.Ble
|
||||||
|
|
@ -69,10 +73,10 @@ class WriteThermometerCallback(
|
||||||
|
|
||||||
if(request.tx != null || request.saveHistory != null || request.historyInterval != null) {
|
if(request.tx != null || request.saveHistory != null || request.historyInterval != null) {
|
||||||
|
|
||||||
fun UInt.to4ByteArrayInBigEndian(): ByteArray =
|
fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
|
||||||
(3 downTo 0).map {
|
(3 downTo 0).map {
|
||||||
(this shr (it * Byte.SIZE_BITS)).toByte()
|
(this shr (it * Byte.SIZE_BITS)).toByte()
|
||||||
}.reversed().toByteArray()
|
}.toByteArray()
|
||||||
|
|
||||||
var uuid: Pair<UUID, ByteArray>? = null
|
var uuid: Pair<UUID, ByteArray>? = null
|
||||||
|
|
||||||
|
|
@ -85,7 +89,7 @@ class WriteThermometerCallback(
|
||||||
Pair(
|
Pair(
|
||||||
intervalWriteUUID,
|
intervalWriteUUID,
|
||||||
mutableListOf<Byte>(3).apply {
|
mutableListOf<Byte>(3).apply {
|
||||||
addAll((it).toUInt().to4ByteArrayInBigEndian().toList())
|
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
|
||||||
}.toByteArray()
|
}.toByteArray()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -203,18 +207,20 @@ class WriteThermometerCallback(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BluetoothGatt.writeCharacteristic(
|
private fun BluetoothGatt.writeCharacteristic(
|
||||||
characteristic: BluetoothGattCharacteristic,
|
characteristic: BluetoothGattCharacteristic,
|
||||||
data: ByteArray
|
data: ByteArray
|
||||||
): Result<Unit, BleException> {
|
): Result<Unit, BleException> {
|
||||||
|
|
||||||
return if(checkPermission()){
|
return if(checkPermission()){
|
||||||
|
|
||||||
|
Log.d("write", data.asUByteArray().joinToString(" ") { it.toString(16).padStart(2, '0') })
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
|
||||||
}else{
|
}else{
|
||||||
|
|
||||||
characteristic.writeType
|
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
characteristic.value = data
|
characteristic.value = data
|
||||||
writeCharacteristic(characteristic)
|
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) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
|
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
package llc.arma.ble.domain.model
|
package llc.arma.ble.domain.model
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
data 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,
|
||||||
|
val scanTime: Long
|
||||||
){
|
){
|
||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue