This commit is contained in:
Vineyro 2023-03-27 17:04:27 +07:00
parent ffba61a55e
commit ce95316494
16 changed files with 448 additions and 172 deletions

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="55381e0a" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-03-22T08:48:05.794601500Z" />
</component>
</project>

View File

@ -2,11 +2,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> android:usesPermissionFlags="neverForLocation"
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui package llc.arma.ble.app.ui
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -7,28 +8,65 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import llc.arma.ble.app.ui.screen.main.MainScreen import llc.arma.ble.app.ui.screen.main.MainScreen
import llc.arma.ble.app.ui.theme.BleTheme import llc.arma.ble.app.ui.theme.BleTheme
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
BleTheme { BleTheme {
Surface( Surface(
modifier = Modifier.fillMaxSize().navigationBarsPadding(), modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
val multiplePermissionsState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
rememberMultiplePermissionsState(
listOf(
android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT
)
)
} else {
rememberMultiplePermissionsState(
listOf()
)
}
if(multiplePermissionsState.allPermissionsGranted) {
MainScreen() MainScreen()
} else {
LaunchedEffect(multiplePermissionsState){
multiplePermissionsState.launchMultiplePermissionRequest()
} }
} }
} }
} }
}
}
} }

View File

@ -18,6 +18,7 @@ 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
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
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.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -49,6 +50,13 @@ fun BleListScreen(
} }
) )
if(state.bleList.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.fillMaxWidth().height(3.dp)
)
}
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()

View File

@ -18,9 +18,17 @@ class BleListViewModel @Inject constructor(
init { init {
getBleAroundFlow().onEach { getBleAroundFlow().onEach {
it.fold(
onSuccess = {
setState { setState {
BleListContract.State(it) BleListContract.State(it)
} }
},
onFailure = {
}
)
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }

View File

@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -123,7 +124,11 @@ fun ConnectionScreen(
private fun LoadingState(){ private fun LoadingState(){
Column { Column {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center
)
)
} }
} }
} }

View File

@ -26,6 +26,31 @@ enum class SheetPage {
INTERVAL, POWER, TEMPERATURE_HISTORY INTERVAL, POWER, TEMPERATURE_HISTORY
} }
private val Boolean.localizedName: String
get() {
return if(this){
"Включено"
} else {
"Выключено"
}
}
private val Ble.BleState.TX.localizedName: String
get() {
return when(this){
Ble.BleState.TX.MINUS_40 -> -40
Ble.BleState.TX.MINUS_20 -> -20
Ble.BleState.TX.MINUS_16 -> -16
Ble.BleState.TX.MINUS_12 -> -12
Ble.BleState.TX.MINUS_8 -> -8
Ble.BleState.TX.MINUS_4 -> -4
Ble.BleState.TX.ZERO -> 0
Ble.BleState.TX.PLUS_3 -> 3
Ble.BleState.TX.PLUS_4 -> 4
}.toString()
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ThermometerScreen( fun ThermometerScreen(
@ -140,8 +165,6 @@ fun ThermometerScreen(
) )
} }
} }
@ -197,7 +220,7 @@ fun ThermometerScreen(
Text( Text(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
text = "${it} db" text = "${state.origin.state.tx.localizedName} db -> ${it.localizedName} db"
) )
} }
@ -209,6 +232,39 @@ fun ThermometerScreen(
it.writeRequest.saveHistory?.let { it.writeRequest.saveHistory?.let {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${state.origin.thermometerState.saveHistory.localizedName} -> ${it.localizedName}"
)
}
}
}
} }
it.writeRequest.historyInterval?.let { it.writeRequest.historyInterval?.let {

View File

@ -119,19 +119,6 @@ fun DisplayState(
} }
if (ble.thermometerState.temperature.loading) {
CircularProgressIndicator()
} else {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
} }
} }
@ -194,7 +181,7 @@ fun DisplayState(
) { ) {
Text( Text(
text = "Интервал измерний" text = "Интервал измерений"
) )
Text( Text(
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,

View File

@ -1,5 +1,9 @@
package llc.arma.ble.app.ui.screen.thermometer.view package llc.arma.ble.app.ui.screen.thermometer.view
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -32,9 +36,15 @@ import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.ChartEntry import com.patrykandpatrick.vico.core.entry.ChartEntry
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -80,7 +90,7 @@ fun TemperatureHistory(
onClick = { onClick = {
viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial)) viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial))
}, },
enabled = state is TemperatureHistoryContract.State.Display enabled = state.loadingHistoryState is ProgressState.Finished
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.Refresh, imageVector = Icons.Rounded.Refresh,
@ -92,10 +102,12 @@ fun TemperatureHistory(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
when (state) { when (state.loadingHistoryState) {
is TemperatureHistoryContract.State.Display -> { is ProgressState.Finished -> {
val producer = state.history.mapIndexed { index, measurePoint -> Text(text = "${state.loadingHistoryState.data.size}")
val producer = state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let { TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let {
ChartEntryModelProducer(it) ChartEntryModelProducer(it)
} }
@ -107,14 +119,20 @@ fun TemperatureHistory(
.orEmpty() .orEmpty()
} }
val lineChart = lineChart() val lineChart = lineChart(
spacing = 110.dp
)
Box(modifier = Modifier.padding(8.dp)) { Box(modifier = Modifier.padding(8.dp)) {
val scrollState = rememberChartScrollState()
LaunchedEffect(scrollState.maxValue){
scrollState.scrollBy(scrollState.maxValue)
}
Chart( Chart(
modifier = Modifier chartScrollState = scrollState,
.fillMaxWidth()
.aspectRatio(1.5f),
chart = lineChart, chart = lineChart,
chartModelProducer = producer, chartModelProducer = producer,
startAxis = startAxis(), startAxis = startAxis(),
@ -122,12 +140,14 @@ fun TemperatureHistory(
valueFormatter = axisValueFormatter, valueFormatter = axisValueFormatter,
labelRotationDegrees = 0f, labelRotationDegrees = 0f,
), ),
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1.5f),
) )
} }
} }
is TemperatureHistoryContract.State.Loading -> { is ProgressState.Indeterminate -> {
Box(modifier = Modifier.padding(8.dp)) { Box(modifier = Modifier.padding(8.dp)) {
@ -138,16 +158,38 @@ fun TemperatureHistory(
){ ){
CircularProgressIndicator( CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} }
} }
}
is ProgressState.Progress -> Box(modifier = Modifier.padding(8.dp)) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(2f),
){
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(durationMillis = progressAnimDuration, easing = FastOutSlowInEasing)
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
} }
} }
}
} }
@ -163,15 +205,9 @@ class TemperatureHistoryContract {
} }
sealed class State : ViewState { class State(
val loadingHistoryState : ProgressState<List<Ble.Thermometer.MeasurePoint>>
object Loading : State() ) : ViewState
data class Display(
var history : List<Ble.Thermometer.MeasurePoint>
) : State()
}
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
@ -186,7 +222,9 @@ class TemperatureHistoryViewModel @Inject constructor(
private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() { ) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() {
override fun setInitialState() = TemperatureHistoryContract.State.Loading override fun setInitialState() = TemperatureHistoryContract.State(
ProgressState.Indeterminate
)
override fun handleEvents(event: TemperatureHistoryContract.Event) { override fun handleEvents(event: TemperatureHistoryContract.Event) {
when(event){ when(event){
@ -201,14 +239,21 @@ class TemperatureHistoryViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
setState { setState {
TemperatureHistoryContract.State.Loading TemperatureHistoryContract.State(ProgressState.Indeterminate)
} }
val history = getTemperatureHistoryBySerial(event.serial) getTemperatureHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState { setState {
TemperatureHistoryContract.State.Display(history) TemperatureHistoryContract.State(it)
} }
},
onFailure = {
}
)
}.launchIn(this)
} }
} }

View File

@ -8,24 +8,23 @@ import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.ParcelUuid
import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
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.ProgressState
import llc.arma.ble.domain.model.Ble 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.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
import java.nio.ByteBuffer
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
@Singleton @Singleton
class BleRepositoryImpl @Inject constructor( class BleRepositoryImpl @Inject constructor(
@ -69,12 +68,21 @@ 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>()
override fun getBleAroundFlow(): Flow<List<BleInfo>> {
val resultList = mutableMapOf<String, BleInfo>() val resultList = mutableMapOf<String, BleInfo>()
return callbackFlow { override fun getBleAroundFlow(): Flow<Result<List<BleInfo>, BleException>> {
return if (ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_SCAN
) != PackageManager.PERMISSION_GRANTED
) {
flow { emit(Result.failure(BleException.PermissionDenied)) }
} else {
callbackFlow {
val bleCallback = object : ScanCallback() { val bleCallback = object : ScanCallback() {
@ -99,13 +107,21 @@ class BleRepositoryImpl @Inject constructor(
} }
} else {
CoroutineScope(Dispatchers.IO).launch {
send(
Result.failure(BleException.PermissionDenied)
)
}
} }
} }
} }
val bleScanner = app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner val bleScanner =
app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner
bleScanner.startScan( bleScanner.startScan(
listOf(), listOf(),
ScanSettings.Builder() ScanSettings.Builder()
@ -121,7 +137,7 @@ class BleRepositoryImpl @Inject constructor(
schedule(object : TimerTask() { schedule(object : TimerTask() {
override fun run() { override fun run() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
send(resultList.values.toList()) send(Result.success(resultList.values.toList()))
} }
} }
}, 100, 500) }, 100, 500)
@ -136,6 +152,8 @@ class BleRepositoryImpl @Inject constructor(
} }
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun getBleBySerial( override suspend fun getBleBySerial(
serial: String serial: String
@ -178,10 +196,17 @@ class BleRepositoryImpl @Inject constructor(
BleInfo.Type.THERMOMETER -> { BleInfo.Type.THERMOMETER -> {
val thermometer = readThermometerState(result).fold(
onFailure = { _ ->
return@launch it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied))
},
onSuccess = { return@fold it }
)
Ble.Thermometer( Ble.Thermometer(
info = info, info = info,
state = state, state = state,
thermometerState = readThermometerState(result) thermometerState = thermometer
) )
} }
@ -204,33 +229,54 @@ class BleRepositoryImpl @Inject constructor(
private suspend fun readThermometerState( private suspend fun readThermometerState(
record: ScanResult record: ScanResult
): Ble.Thermometer.ThermometerState { ): Result<Ble.Thermometer.ThermometerState, BleException> {
return Ble.Thermometer.ThermometerState( val temperature = readTemperature(record).fold(
temperature = readTemperature(record), onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
)
val history = readHistoryInterval(record).fold(
onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
)
return Result.success(
Ble.Thermometer.ThermometerState(
temperature = temperature,
saveHistory = record.timerEnabled, saveHistory = record.timerEnabled,
historyInterval = readHistoryInterval(record) historyInterval = history
)
) )
} }
private suspend fun readTemperature( private suspend fun readTemperature(
record: ScanResult record: ScanResult
): Float { ): Result<Float, BleException> {
val dataResult = readCharacteristic( val dataResult = readCharacteristic(
device = record.device, device = record.device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb") characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
).fold(
onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
) )
return (dataResult[0] + dataResult[1] * 256).toFloat() / 100f return Result.success((dataResult[0] + dataResult[1] * 256).toFloat() / 100f)
} }
private suspend fun readHistoryInterval( private suspend fun readHistoryInterval(
record: ScanResult record: ScanResult
): Long { ): Result<Long, BleException> {
writeCharacteristic( writeCharacteristic(
device = record.device, device = record.device,
@ -243,19 +289,26 @@ class BleRepositoryImpl @Inject constructor(
device = record.device, device = record.device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
).fold(
onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
) )
return if(dataResult.size == 4){ return Result.success(
if(dataResult.size == 4){
dataResult.getUIntAt(0).toLong() dataResult.getUIntAt(0).toLong()
}else{ }else{
0 0
} }
)
} }
override suspend fun getTemperatureHistoryBySerial( override suspend fun getTemperatureHistoryBySerial(
serial: String serial: String
): List<Ble.Thermometer.MeasurePoint> { ): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> = flow {
fun ByteArray.getUIntAt(idx: Int) = fun ByteArray.getUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or ((this[idx + 3].toUInt() and 0xFFu) shl 24) or
@ -265,6 +318,8 @@ class BleRepositoryImpl @Inject constructor(
deviceCache[serial]?.device?.let { device -> deviceCache[serial]?.device?.let { device ->
emit(Result.success(ProgressState.Indeterminate))
writeCharacteristic( writeCharacteristic(
device = device, device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
@ -276,6 +331,12 @@ class BleRepositoryImpl @Inject constructor(
device = device, device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
) )
writeCharacteristic( writeCharacteristic(
@ -295,6 +356,12 @@ class BleRepositoryImpl @Inject constructor(
device = device, device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
) )
if(firstPackageResponse[0] == 250.toByte()){ if(firstPackageResponse[0] == 250.toByte()){
@ -311,11 +378,14 @@ class BleRepositoryImpl @Inject constructor(
(it[0] + it[1] * 256).toFloat() / 100f (it[0] + it[1] * 256).toFloat() / 100f
}.toMutableList() }.toMutableList()
Log.d("read", temperaturePackage.size.toString()) var dataCount = firstPackageResponse[1].toUByte()
val totalDataSize = dataCount.toInt() + temperaturePackage.size
var dataCount = firstPackageResponse[1] emit(Result.success(ProgressState.Progress(0f / totalDataSize.toFloat())))
delay(100)
emit(Result.success(ProgressState.Progress(dataCount.toFloat() / totalDataSize.toFloat())))
while(dataCount != 0.toByte()){ while(dataCount != 0.toUByte()){
writeCharacteristic( writeCharacteristic(
device = device, device = device,
@ -328,9 +398,15 @@ class BleRepositoryImpl @Inject constructor(
device = device, device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
).fold(
onFailure = {
emit(Result.failure(it))
return@flow
},
onSuccess = { return@fold it }
) )
dataCount = readResponse.get(1) dataCount = readResponse[1].toUByte()
temperatureDataArray = readResponse.toList().subList(2, readResponse.size) temperatureDataArray = readResponse.toList().subList(2, readResponse.size)
@ -340,25 +416,27 @@ class BleRepositoryImpl @Inject constructor(
} }
) )
Log.d("read",(temperatureDataArray.size / 2).toString()) emit(Result.success(ProgressState.Progress(totalDataSize.toFloat() / temperaturePackage.size.toFloat())))
} }
Log.d("metadata", interval.toString() + " " + lastMeasureSystemTime.toString()) emit(
Result.success(
return temperaturePackage.withIndex().map { ProgressState.Finished(
temperaturePackage.withIndex().map {
Ble.Thermometer.MeasurePoint( Ble.Thermometer.MeasurePoint(
date = lastMeasureSystemTime - (((temperaturePackage.size - 1) - it.index) * interval), date = lastMeasureSystemTime - (((temperaturePackage.size - 1) - it.index) * interval),
value = it.value value = it.value
) )
} }
)
)
)
} }
} }
return emptyList()
} }
override suspend fun writeBle(ble: Ble) { override suspend fun writeBle(ble: Ble) {
@ -381,6 +459,9 @@ class BleRepositoryImpl @Inject constructor(
request.saveHistory?.let { writeSaveEnabled(result.device, it) } request.saveHistory?.let { writeSaveEnabled(result.device, it) }
deviceCache.remove(serial)
resultList.remove(serial)
} }
} }
@ -457,24 +538,26 @@ class BleRepositoryImpl @Inject constructor(
private suspend fun writeSaveEnabled( private suspend fun writeSaveEnabled(
device: BluetoothDevice, device: BluetoothDevice,
enabled: Boolean enabled: Boolean
) { ): Result<Unit, BleException> {
writeCharacteristic( writeCharacteristic(
device = device, device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"), characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
writeData = mutableListOf<Byte>(3).apply { writeData = mutableListOf<Byte>(4).apply {
add(if(enabled) 1 else 0) add(if(enabled) 1 else 0)
}.toByteArray() }.toByteArray()
) )
return Result.success(Unit)
} }
private suspend fun readCharacteristic( private suspend fun readCharacteristic(
device: BluetoothDevice, device: BluetoothDevice,
serviceId: UUID, serviceId: UUID,
characteristicId: UUID characteristicId: UUID
): ByteArray = suspendCoroutine { ): Result<ByteArray, BleException> = suspendCancellableCoroutine {
val callback = object : BluetoothGattCallback() { val callback = object : BluetoothGattCallback() {
@ -492,10 +575,10 @@ class BleRepositoryImpl @Inject constructor(
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
gatt?.discoverServices() gatt?.discoverServices()
} else {
it.resume(Result.failure(BleException.PermissionDenied))
} }
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
} }
} }
@ -512,16 +595,16 @@ class BleRepositoryImpl @Inject constructor(
service.uuid == serviceId service.uuid == serviceId
}?.characteristics?.firstOrNull { characteristic -> }?.characteristics?.firstOrNull { characteristic ->
characteristic.uuid == characteristicId characteristic.uuid == characteristicId
}?.let { }?.let { char ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
app, app,
Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
gatt.readCharacteristic(char)
gatt.readCharacteristic(it) } else {
it.resume(Result.failure(BleException.PermissionDenied))
} }
} }
@ -537,14 +620,29 @@ class BleRepositoryImpl @Inject constructor(
status: Int status: Int
) { ) {
super.onCharacteristicRead(gatt, characteristic, value, status) super.onCharacteristicRead(gatt, characteristic, value, status)
if (ActivityCompat.checkSelfPermission(
it.resume(value) app,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
it.resume(Result.failure(BleException.PermissionDenied))
}else {
gatt.disconnect()
it.resume(Result.success(value))
}
} }
} }
if (ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
it.resume(Result.failure(BleException.PermissionDenied))
} else {
device.connectGatt(app, true, callback) device.connectGatt(app, true, callback)
}
} }
@ -553,7 +651,7 @@ class BleRepositoryImpl @Inject constructor(
serviceId: UUID, serviceId: UUID,
characteristicId: UUID, characteristicId: UUID,
writeData: ByteArray writeData: ByteArray
) = suspendCoroutine { ) = suspendCancellableCoroutine {
val callback = object : BluetoothGattCallback() { val callback = object : BluetoothGattCallback() {
@ -573,8 +671,6 @@ class BleRepositoryImpl @Inject constructor(
gatt?.discoverServices() gatt?.discoverServices()
} }
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
} }
} }
@ -591,7 +687,7 @@ class BleRepositoryImpl @Inject constructor(
service.uuid == serviceId service.uuid == serviceId
}?.characteristics?.firstOrNull { characteristic -> }?.characteristics?.firstOrNull { characteristic ->
characteristic.uuid == characteristicId characteristic.uuid == characteristicId
}?.let { }?.let { char ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
app, app,
@ -600,10 +696,10 @@ class BleRepositoryImpl @Inject constructor(
) { ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeCharacteristic(it, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) gatt.writeCharacteristic(char, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{ }else{
it.value = writeData char.value = writeData
gatt.writeCharacteristic(it) gatt.writeCharacteristic(char)
} }
} }
@ -620,11 +716,21 @@ class BleRepositoryImpl @Inject constructor(
status: Int status: Int
) { ) {
super.onCharacteristicWrite(gatt, characteristic, status) super.onCharacteristicWrite(gatt, characteristic, status)
if (ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
return
} else {
gatt.disconnect()
it.resume(Unit) it.resume(Unit)
} }
} }
}
device.connectGatt(app, true, callback) device.connectGatt(app, true, callback)
} }

View File

@ -0,0 +1,7 @@
package llc.arma.ble.domain.common
sealed class BleException {
object PermissionDenied : BleException()
}

View File

@ -0,0 +1,15 @@
package llc.arma.ble.domain.common
sealed class ProgressState<out T> {
object Indeterminate : ProgressState<Nothing>()
data class Progress(
val value: Float
) : ProgressState<Nothing>()
data class Finished<T>(
val data: T
) : ProgressState<T>()
}

View File

@ -2,17 +2,19 @@ package llc.arma.ble.domain.repository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.ProgressState
import llc.arma.ble.domain.model.Ble 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.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
interface BleRepository { interface BleRepository {
fun getBleAroundFlow(): Flow<List<BleInfo>> fun getBleAroundFlow(): Flow<Result<List<BleInfo>, BleException>>
suspend fun getBleBySerial(serial: String): Result<Ble, GetBleBySerial.GetBleException> suspend fun getBleBySerial(serial: String): Result<Ble, GetBleBySerial.GetBleException>
suspend fun getTemperatureHistoryBySerial(serial: String): List<Ble.Thermometer.MeasurePoint> suspend fun getTemperatureHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>>
suspend fun writeBle(ble: Ble) suspend fun writeBle(ble: Ble)

View File

@ -1,6 +1,8 @@
package llc.arma.ble.domain.usecase package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
@ -10,6 +12,6 @@ class GetBleAroundFlow @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository
) { ) {
operator fun invoke(): Flow<List<BleInfo>> = bleRepository.getBleAroundFlow() operator fun invoke(): Flow<Result<List<BleInfo>, BleException>> = bleRepository.getBleAroundFlow()
} }

View File

@ -1,5 +1,9 @@
package llc.arma.ble.domain.usecase package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject import javax.inject.Inject
@ -8,7 +12,7 @@ class GetTemperatureHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository
) { ) {
suspend operator fun invoke(serial: String): List<Ble.Thermometer.MeasurePoint> { suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> {
return bleRepository.getTemperatureHistoryBySerial(serial) return bleRepository.getTemperatureHistoryBySerial(serial)

View File

@ -13,7 +13,10 @@ class WriteBle @Inject constructor(
bleRepository.writeBle(ble) bleRepository.writeBle(ble)
} }
suspend operator fun invoke(serial: String, request: Ble.Thermometer.WriteRequest){ suspend operator fun invoke(
serial: String,
request: Ble.Thermometer.WriteRequest
){
bleRepository.writeBle(serial, request) bleRepository.writeBle(serial, request)
} }