Conflicts:
	.idea/misc.xml
This commit is contained in:
Vineyro 2023-06-05 11:40:43 +07:00
commit e8853d8cda
10 changed files with 753 additions and 45 deletions

View File

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

View File

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

View File

@ -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,10 +269,20 @@ private fun BleItem(
contentDescription = null contentDescription = null
) )
Text( Box {
style = MaterialTheme.typography.bodyMedium,
text = ble.rssi.toString() + " dBm" 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) 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
) )
Text( Box {
style = MaterialTheme.typography.bodyMedium,
text = ble.batteryLevel.toString() + " %" 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"
}
)
}
}
} }
} }

View File

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

View File

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

View File

@ -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 мин."
) )
} }

View File

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

View File

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

View File

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

View File

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