Add accelerometer history

This commit is contained in:
Vineyro 2023-07-18 15:02:11 +07:00
parent c47c371530
commit 4528a7cd9a
31 changed files with 2302 additions and 98 deletions

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.10" />
</component>
</project>

View File

@ -13,8 +13,8 @@ android {
applicationId "llc.arma.ble"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -56,6 +56,10 @@ class MainActivity : ComponentActivity() {
mutableStateOf<@Composable () -> Unit>({})
}
if(modalState.currentValue == ModalBottomSheetValue.Hidden){
sheetContent = {}
}
CompositionLocalProvider(
LocalBottomDialogState provides BottomState(
sheetState = modalState,

View File

@ -0,0 +1,50 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun SignalLevel(
modifier: Modifier = Modifier,
maxLevel: Int = 5,
level: Int
){
val step = (16 - 4) / 4
Row(
modifier = modifier.height(16.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.Bottom
) {
for(col in 0..4 step 1){
Surface(
color = LocalContentColor.current.copy(
alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled
),
shape = CircleShape,
modifier = Modifier
.width(4.dp)
.defaultMinSize(minHeight = 4.dp)
.height(((col + 1) * step).dp)
) { }
}
}
}

View File

@ -35,6 +35,9 @@ class BleMapper @Inject constructor(
is Ble.Accelerometer -> {
BleView.Accelerometer(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
)
}
}

View File

@ -34,7 +34,10 @@ class BleViewMapper @Inject constructor(
is BleView.Accelerometer -> {
Ble.Accelerometer(
info = input.info
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
)
}
}

View File

@ -10,7 +10,8 @@ sealed class BleView(
) {
class Accelerometer(
info: BleInfo
info: BleInfo,
val state: BleState
) : BleView(info)
class Beacon(
@ -58,9 +59,27 @@ sealed class BleView(
MINUS_4(-4),
ZERO(0),
PLUS_3(3),
PLUS_4(4)
PLUS_4(4);
val powerPercentage: Int
get() {
return when(this){
MINUS_40 -> 1
MINUS_20 -> 5
MINUS_16 -> 7
MINUS_12 -> 10
MINUS_8 -> 16
MINUS_4 -> 20
ZERO -> 40
PLUS_3 -> 80
PLUS_4 -> 100
}
}
}
}
}

View File

@ -15,6 +15,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -29,9 +30,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -184,6 +187,16 @@ private fun ItemIcon(
}
private fun Int.toSignalLevel(): Int {
return when(this){
in -30 downTo -52 -> 4
in -51 downTo -63 -> 3
in -62 downTo -75 -> 2
in -74 downTo -89 -> 1
else -> 0
}
}
@Composable
private fun BleItem(
ble: BleInfo,
@ -254,6 +267,41 @@ private fun BleItem(
text = ble.serial
)
/*Text(
style = MaterialTheme.typography.bodyMedium,
text = String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20)))
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = String.format("%.3f", (ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0)))
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.tx.toString() + " tx"
)*/
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
Text(
style = MaterialTheme.typography.bodyMedium,
text = String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
@ -263,11 +311,9 @@ private fun BleItem(
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.NetworkCell,
contentDescription = null
)
SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
Spacer(modifier = Modifier.width(4.dp))
Box {

View File

@ -15,10 +15,26 @@ class AccelerometerContract {
object OnHideAccelerometerMeasure : Event()
object OnShowAccelerometerHistory : Event()
object OnHideAccelerometerHistory : Event()
object OnPowerEdit : Event()
object OnShowWriteBlePreview : Event()
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
data class OnBleChanged(
val ble: Ble.Accelerometer,
): Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
}
sealed class State : ViewState {
@ -28,7 +44,26 @@ class AccelerometerContract {
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer,
) : State()
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
object Success : WriteState()
object Failure : WriteState()
}
}
}
@ -36,6 +71,16 @@ class AccelerometerContract {
object ShowAccelerometerMeasure : Effect()
object ShowAccelerometerHistory : Effect()
object ShowPowerPicker : Effect()
object HidePowerPicker : Effect()
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
}
}

View File

@ -1,12 +1,12 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import androidx.compose.foundation.layout.Column
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel
@ -15,16 +15,18 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerHistory
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerMeasure
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.PowerEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.Write
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
MEASURE_HISTORY
MEASURE, POWER, WRITE, HISTORY
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AccelerometerScreen(
ble: Ble.Accelerometer,
@ -43,9 +45,11 @@ fun AccelerometerScreen(
mutableStateOf<SheetPage?>(null)
}
val scope = rememberCoroutineScope()
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.MEASURE_HISTORY -> launch {
SheetPage.MEASURE -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
@ -54,19 +58,93 @@ fun AccelerometerScreen(
}
}
}
SheetPage.HISTORY -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerHistory(ble = currentState.accelerometer.info)
}
}
}
SheetPage.POWER -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
PowerEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
}
null -> {
bottomDialog.hide()
}
}
}
DisposableEffect(key1 = Unit, effect = {
onDispose {
scope.launch {
bottomDialog.hide()
}
}
})
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
AccelerometerContract.Effect.ShowAccelerometerMeasure -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.MEASURE_HISTORY
sheetPage = SheetPage.MEASURE
}
is AccelerometerContract.Effect.HidePowerPicker -> launch {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER
}
is AccelerometerContract.Effect.HideWriteBle -> {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowWriteBle -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
AccelerometerContract.Effect.ShowAccelerometerHistory -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY
}
}
}.launchIn(this)

View File

@ -1,16 +1,22 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class AccelerometerViewModel @Inject constructor(
private val bleMapper: BleMapper
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
override fun setInitialState() = AccelerometerContract.State.Loading
@ -20,6 +26,109 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerMeasure -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerMeasure -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerHistory
) {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerHistory
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
writeState = null
)
}
}
setEffect {
AccelerometerContract.Effect.HideWriteBle
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx
)
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
AccelerometerContract.Effect.ShowWriteBle
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.state.tx = event.tx
}
setEffect {
AccelerometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerEdit
) {
setEffect {
AccelerometerContract.Effect.ShowPowerPicker
}
}
@ -52,7 +161,8 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.State.Display -> setState {
state.copy(
origin = Ble.Accelerometer(
info = event.ble.info
info = event.ble.info,
state = event.ble.state
)
)
}
@ -60,11 +170,76 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.State.Loading -> setState {
AccelerometerContract.State.Display(
origin = event.ble,
accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer
accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer,
writeState = null
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
state.writeState?.let { request ->
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
}
writeBle(state.accelerometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
)
)
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}

View File

@ -0,0 +1,358 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
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.usecase.GetAccelerometerHistoryBySerial
import java.util.*
class AccelerometerEntry(
val frequency: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
}
@Composable
fun AccelerometerHistory(
ble: BleInfo
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.serial))
}
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "История измерений (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "История измерений"
is ProgressState.Progress -> "История измерений"
}
}
AccelerometerHistoryContract.State.Exception -> "История измерений"
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.serial))
},
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerHistoryContract.State.Display -> Display(state = state)
AccelerometerHistoryContract.State.Exception -> Exception()
}
}
}
}
@Composable
fun Display(
state: AccelerometerHistoryContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val producer = remember(state.loadingHistoryState.data) {
state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}.let {
ChartEntryModelProducer(it)
}
}
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.toString()
.orEmpty()
}
val lineChart = columnChart()
val scrollState = rememberChartScrollState()
Chart(
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize(),
)
LaunchedEffect(scrollState.maxValue) {
scrollState.scrollBy(scrollState.maxValue)
}
}
}
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
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)
)
}
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerHistoryContract {
sealed class Event : ViewEvent {
data class OnStart(
val serial: String
) : Event()
data class OnRefreshHistory(
val serial: String
) : Event()
}
sealed class State : ViewState {
data class Display(
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.MeasurePoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerHistoryContract.State.Display(
ProgressState.Indeterminate
)
override fun handleEvents(event: AccelerometerHistoryContract.Event) {
when(event){
is AccelerometerHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import android.util.Log
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
@ -22,10 +23,13 @@ import llc.arma.ble.domain.model.BleInfo
import javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
@ -33,6 +37,7 @@ import com.patrykandpatrick.vico.core.scroll.InitialScroll
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.usecase.Accelereate
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
@Composable
@ -40,17 +45,21 @@ fun AccelerometerMeasure(
ble: BleInfo
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val viewModel = hiltViewModel<AccelerometerMeasureViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
/*LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial))
}
}*/
viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial))
DisposableEffect(key1 = ble, effect = {
DisposableEffect(key1 = ble.serial, effect = {
onDispose {
viewModel.setEvent(AccelerometerMeasureContract.Event.StopMeasure)
}
})
Column(
@ -118,37 +127,104 @@ fun Display(
if (state.measureHistory.isEmpty()) {
Text(
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
strokeCap = StrokeCap.Round
)
} else {
val producer = remember {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
producer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint)
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.x)
})
val lineChart = columnChart()
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.y)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.z)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
)
)
Column() {
Text(text = "Ось X:")
Chart(
chart = lineChart,
chartModelProducer = producer,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Y:")
Chart(
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Z:")
Chart(
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
@ -156,7 +232,7 @@ fun Display(
}
@Composable
fun Exception(
private fun Exception(
) {
Box(
@ -195,7 +271,7 @@ class AccelerometerMeasureContract {
sealed class State : ViewState {
data class Display(
val measureHistory : List<Float>
val measureHistory : List<Accelereate>
) : State()
object Exception : State()
@ -211,7 +287,7 @@ class AccelerometerMeasureContract {
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
class AccelerometerMeasureViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerMeasureContract.State, AccelerometerMeasureContract.Event, AccelerometerMeasureContract.Effect>() {
@ -237,6 +313,8 @@ class AccelerometerHistoryViewModel @Inject constructor(
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerMeasureContract.State.Display(emptyList())
}
@ -262,6 +340,8 @@ class AccelerometerHistoryViewModel @Inject constructor(
private fun startReadMeasure(serial: String, restartJob: Boolean){
if(restartJob || measureJob == null) {
measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch {
setState {

View File

@ -50,6 +50,47 @@ fun DisplayState(
modifier = Modifier,
content = {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnPowerEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${ble.state.tx.value} db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
@ -72,7 +113,43 @@ fun DisplayState(
) {
Text(
text = "График измерений"
text = "График"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "История"
)
}
@ -99,7 +176,7 @@ fun DisplayState(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
}
) {

View File

@ -0,0 +1,97 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
@Composable
fun PowerEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.state.tx) {
mutableStateOf(state.state.tx)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Мощность",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
BleView.BleState.TX.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { value = it }
.padding(4.dp)
) {
RadioButton(
selected = it == value,
onClick = { value = it }
)
Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)")
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
AccelerometerContract.Event.OnPowerChanged(
value
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}

View File

@ -0,0 +1,349 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import llc.arma.ble.R
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
@Composable
fun Write(
state: AccelerometerContract.State.Display.WriteState,
onEvent: (AccelerometerContract.Event) -> Unit
) {
Column(
modifier = Modifier.animateContentSize()
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is AccelerometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null) {
state.writeRequest.tx?.let {
Box(
modifier = Modifier.padding(
vertical = 0.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 = "${it.localizedName} db"
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnWriteBle)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Записать"
)
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
is AccelerometerContract.State.Display.WriteState.Writing -> {
Box {
Column() {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Failure -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -55,7 +55,7 @@ fun PowerEdit(
onClick = { value = it }
)
Text(text = it.value.toString() + " db")
Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)")
}

View File

@ -52,7 +52,6 @@ val Ble.BleState.TX.localizedName: String
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun ThermometerScreen(
ble: Ble.Thermometer,

View File

@ -54,7 +54,7 @@ fun PowerEdit(
onClick = { value = it }
)
Text(text = it.value.toString() + " db")
Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)")
}

View File

@ -221,7 +221,7 @@ fun Display(
}
@Composable
fun Exception(
private fun Exception(
) {
Box(

View File

@ -24,6 +24,7 @@ import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.usecase.Accelereate
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@ -32,9 +33,9 @@ import kotlin.random.Random
val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
val accelerometerDescriptorUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb")
val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val accelerometerHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
val intervalReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val intervalWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
@ -69,7 +70,8 @@ class BleRepositoryImpl @Inject constructor(
batteryLevel = batteryLevel ?: 0,
rssi = rssi,
type = type,
scanTime = timestampNanos / 1_000_000
scanTime = timestampNanos / 1_000_000,
tx = scanRecord?.txPowerLevel ?: 0
)
}
@ -250,7 +252,19 @@ class BleRepositoryImpl @Inject constructor(
} else {
newResult.rssi
}
),
state = Ble.BleState(
tx = when (result.scanRecord?.txPowerLevel) {
-40 -> Ble.BleState.TX.MINUS_40
-20 -> Ble.BleState.TX.MINUS_20
-16 -> Ble.BleState.TX.MINUS_16
-12 -> Ble.BleState.TX.MINUS_12
-8 -> Ble.BleState.TX.MINUS_8
-4 -> Ble.BleState.TX.MINUS_4
3 -> Ble.BleState.TX.PLUS_3
4 -> Ble.BleState.TX.PLUS_4
else -> Ble.BleState.TX.ZERO
}
)
)
@ -484,6 +498,44 @@ class BleRepositoryImpl @Inject constructor(
}
override suspend fun getAccelerometerHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>> {
var gatt: BluetoothGatt? = null
return callbackFlow {
deviceCache[serial]?.device?.let {
if (checkPermission()) {
gatt = it.connectGatt(app, false, ReadAccelerometerHistoryCallback(app) {
CoroutineScope(Dispatchers.IO).launch {
send(it)
}
})
} else {
CoroutineScope(Dispatchers.IO).launch {
send(Result.failure(BleException.PermissionDenied))
}
return@callbackFlow
}
}
awaitClose {
gatt?.close()
}
}
}
override suspend fun getTemperatureHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> {
@ -594,6 +646,42 @@ class BleRepositoryImpl @Inject constructor(
}
override suspend fun writeBle(
serial: String,
request: Ble.Accelerometer.WriteRequest
): Result<Unit, BleException> = suspendCancellableCoroutine {
deviceCache[serial]?.let { scanResult ->
if(checkPermission()) {
var gatt: BluetoothGatt? = null
val callback = WriteAccelerometerCallback(app, request) { result ->
gatt?.close()
result.onSuccess {
deviceCache.remove(serial)
resultList.remove(serial)
}
it.resume(result)
}
gatt = scanResult.device.connectGatt(app, false, callback)
} else {
it.resume(Result.failure(BleException.PermissionDenied))
}
}
}
override suspend fun changeBlePassword(
password: String,
serial: String
@ -618,53 +706,28 @@ class BleRepositoryImpl @Inject constructor(
return Result.success(Unit)
}
override fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Float, BleException>> {
override fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Accelereate, BleException>> {
return callbackFlow {
var gatt: BluetoothGatt? = null
deviceCache[serial]?.let {
it.device.connectGatt(app, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
gatt.discoverServices()
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
gatt.setCharacteristicNotification(it, true)
gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
Log.d("new", value.toString())
gatt = it.device.connectGatt(
app,
false,
ReadAccelerometerCallback(app){ result ->
CoroutineScope(Dispatchers.IO).launch {
send(
Result.success(((-256)..256).random().toFloat())
send(result)
}
}
)
}
}
})
}
awaitClose {
Log.d("acc", "close")
gatt?.close()
}
}

View File

@ -0,0 +1,153 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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.usecase.Accelereate
import java.util.UUID
import java.util.stream.Collectors
class ReadAccelerometerCallback(
private val app: Application,
private val onResult: (Result<Accelereate, BleException>) -> Unit
) : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (checkPermission()) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {
gatt.setCharacteristicNotification(it, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
} else {
val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicChanged(gatt, characteristic)
onCommonCharacteristicRead(gatt, characteristic, characteristic.value)
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
onCommonCharacteristicRead(gatt, characteristic, value)
}
private fun onCommonCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
){
val data = value.toList().chunked(2).map {
it[1].toInt()
}
onResult(
Result.success(
Accelereate(
x = data[0].toFloat(),
y = data[1].toFloat(),
z = data[2].toFloat()
)
)
)
}
private fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
}

View File

@ -0,0 +1,342 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
class ReadAccelerometerHistoryCallback(
private val app: Application,
private val onResult: (Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
private fun ByteArray.get4byteUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or
((this[idx + 2].toUInt() and 0xFFu) shl 16) or
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
private fun ByteArray.get2byteUIntAt(idx: Int) =
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
private var readProperty: Property? = null
init {
onResult(Result.success(ProgressState.Indeterminate))
}
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (checkPermission()) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
Log.d("read", "discover ${status}")
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerHistoryReadUUID)?.let {
if (checkPermission()) {
Log.d("read", "discovered")
readProperty = Property.DATA_SIZE
gatt.writeCharacteristic(it, byteArrayOf(2))
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
return
}
onResult(Result.failure(BleException.UnexpectedResponse))
}
}
private var lastMeasureSystemTime: Long? = null
private var bleMeasureInterval: Long? = null
private var bleRealTime: Long? = null
private var bleLastMeasureTime: Long? = null
private val resultAccelerometerPackage: MutableList<Float> = mutableListOf()
var expectedDataSize: Int? = null
@Deprecated("Deprecated in Java")
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicRead(gatt, characteristic, status)
onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status)
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
onCommonCharacteristicRead(gatt, characteristic, value, status)
}
@OptIn(ExperimentalUnsignedTypes::class)
private fun onCommonCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
){
Log.d("read", value[0].toString())
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){
Property.DATA_SIZE -> {
if(value.contentEquals(byteArrayOf(0, 0))) {
onResult(
Result.success(
ProgressState.Finished(
emptyList()
)
)
)
gatt.close()
} else {
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
readProperty = Property.PACKAGE
gatt.writeCharacteristic(characteristic, writeData)
}
}
Property.PACKAGE -> {
if(value[0] == 250.toByte()){
bleMeasureInterval = value.get4byteUIntAt(4).toLong()
bleLastMeasureTime = value.get4byteUIntAt(8).toLong()
bleRealTime = value.get4byteUIntAt(12).toLong()
lastMeasureSystemTime = ((bleRealTime!! - bleLastMeasureTime!!) * 1_000)
val accelerometerDataArray = value.toUByteArray().asList().subList(16, value.size)
resultAccelerometerPackage.addAll(
accelerometerDataArray.chunked(2).map {
it.toUByteArray().toTemperature()
}.toMutableList()
)
val nextPackageDataCount = value.get2byteUIntAt(2)
expectedDataSize = nextPackageDataCount.toInt() + resultAccelerometerPackage.size
Log.d("read", expectedDataSize.toString())
onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat())))
onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat())))
if(nextPackageDataCount != 0.toUInt()){
if (checkPermission()) {
gatt.writeCharacteristic(characteristic, byteArrayOf(5))
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(
Result.success(
ProgressState.Finished(
resultAccelerometerPackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = lastMeasureSystemTime!! - (((resultAccelerometerPackage.size - 1) - it.index) * bleMeasureInterval!!),
value = it.value
)
}
)
)
)
gatt.close()
}
} else {
if (value[0] == 251.toByte()) {
val nextPackageDataCount = value.get2byteUIntAt(2)
val temperatureDataArray = value.toUByteArray().toList().subList(4, value.size)
resultAccelerometerPackage.addAll(
temperatureDataArray.chunked(2).map {
it.toUByteArray().toTemperature()
}
)
onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat())))
if (nextPackageDataCount != 0.toUInt()) {
val writeData = byteArrayOf(5)
gatt.writeCharacteristic(characteristic, writeData)
gatt.readCharacteristic(characteristic)
} else {
onResult(
Result.success(
ProgressState.Finished(
resultAccelerometerPackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = lastMeasureSystemTime!! - (((resultAccelerometerPackage.size - 1) - it.index) * bleMeasureInterval!!),
value = it.value
)
}
)
)
)
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
else -> {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if(status == BluetoothGatt.GATT_SUCCESS){
if (checkPermission()) {
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException>{
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
}

View File

@ -0,0 +1,209 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothProfile
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import java.util.UUID
class WriteAccelerometerCallback(
private val app: Application,
private var request: Ble.Accelerometer.WriteRequest,
private val onResult: (Result<Unit, BleException>) -> Unit
) : BluetoothGattCallback() {
private var flashed = false
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)
}
private fun onCycle(
gatt: BluetoothGatt,
status: Int
){
if(request.tx != null) {
var uuid: Pair<UUID, ByteArray>? = null
uuid = request.tx?.let {
this.request = request.copy(
tx = null
)
Pair(
txWriteUUID,
byteArrayOf(
when (it) {
Ble.BleState.TX.MINUS_40 -> -40
Ble.BleState.TX.MINUS_20 -> -20
Ble.BleState.TX.MINUS_16 -> -16
Ble.BleState.TX.MINUS_12 -> -12
Ble.BleState.TX.MINUS_8 -> -8
Ble.BleState.TX.MINUS_4 -> -4
Ble.BleState.TX.ZERO -> 0
Ble.BleState.TX.PLUS_3 -> 3
Ble.BleState.TX.PLUS_4 -> 4
}
)
)
} ?: uuid
uuid?.let { uuid ->
gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull {
it.uuid == uuid.first
}?.let {
gatt.writeCharacteristic(it, uuid.second)
return
}
}
onResult(Result.failure(BleException.UnexpectedResponse))
} else {
if(flashed.not()){
flashed = true
gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull {
it.uuid == flashWriteUUID
}?.let {
gatt.writeCharacteristic(it, byteArrayOf(9))
return
}
onResult(Result.failure(BleException.UnexpectedResponse))
} else {
onResult(Result.success(Unit))
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
Log.d("beacon", "onCharacteristicWrite $status")
super.onCharacteristicWrite(gatt, characteristic, status)
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS || flashed) {
onCycle(gatt, status)
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
}
}
fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException> {
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.writeType
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
}

View File

@ -30,8 +30,6 @@ class WriteBeaconCallback(
) {
super.onConnectionStateChange(gatt, status, newState)
Log.d("beacon", "onConnectionStateChange $status $newState")
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
@ -56,7 +54,6 @@ class WriteBeaconCallback(
gatt: BluetoothGatt,
status: Int
) {
Log.d("beacon", "onServicesDiscovered $status")
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)

View File

@ -5,9 +5,19 @@ sealed class Ble(
) {
class Accelerometer(
info: BleInfo
info: BleInfo,
val state: BleState
): Ble(info) {
data class WriteRequest(
val tx: BleState.TX?,
)
class MeasurePoint (
val frequency: Long,
val value: Float
)
}
class Beacon(

View File

@ -6,7 +6,8 @@ data class BleInfo(
val batteryLevel: Int,
val rssi: Int?,
val type: Type,
val scanTime: Long
val scanTime: Long,
val tx: Int
){
enum class Type {

View File

@ -7,6 +7,7 @@ import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import llc.arma.ble.domain.usecase.Accelereate
import llc.arma.ble.domain.usecase.GetBleBySerial
interface BleRepository {
@ -23,7 +24,12 @@ interface BleRepository {
suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(serial: String, request: Ble.Accelerometer.WriteRequest): Result<Unit, BleException>
suspend fun changeBlePassword(password: String, serial: String): Result<Unit, BleException>
fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Float, BleException>>
fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Accelereate, BleException>>
suspend fun getAccelerometerHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>>
}

View File

@ -0,0 +1,21 @@
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.repository.BleRepository
import javax.inject.Inject
class GetAccelerometerHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository
) {
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>> {
return bleRepository.getAccelerometerHistoryBySerial(serial)
}
}

View File

@ -12,10 +12,16 @@ class GetAccelerometerMeasureBySerialFlow @Inject constructor(
private val bleRepository: BleRepository
) {
operator fun invoke(serial: String): Flow<Result<Float, BleException>> {
operator fun invoke(serial: String): Flow<Result<Accelereate, BleException>> {
return bleRepository.getAccelerometerMeasureBySerialFlow(serial)
}
}
data class Accelereate(
val x: Float,
val y: Float,
val z: Float,
)

View File

@ -24,4 +24,11 @@ class WriteBle @Inject constructor(
return bleRepository.writeBle(serial, request)
}
suspend operator fun invoke(
serial: String,
request: Ble.Accelerometer.WriteRequest
): llc.arma.ble.domain.Result<Unit, BleException>{
return bleRepository.writeBle(serial, request)
}
}