История изменений акселерометра

This commit is contained in:
Vineyro 2023-09-07 16:22:40 +07:00
parent df30906056
commit c59c929f35
31 changed files with 1894 additions and 396 deletions

View File

@ -14,7 +14,7 @@ android {
minSdk 24
targetSdk 33
versionCode 5
versionName "1.2.2"
versionName "1.2.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@ -38,6 +38,10 @@ class BleMapper @Inject constructor(
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = BleView.Accelerometer.AccelerometerState(
saveHistory = input.accelerometerState.saveHistory,
historyInterval = input.accelerometerState.historyInterval,
)
)
}
}

View File

@ -38,6 +38,10 @@ class BleViewMapper @Inject constructor(
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = Ble.Accelerometer.AccelerometerState(
saveHistory = input.accelerometerState.saveHistory,
historyInterval = input.accelerometerState.historyInterval,
)
)
}
}

View File

@ -11,8 +11,21 @@ sealed class BleView(
class Accelerometer(
info: BleInfo,
val state: BleState
) : BleView(info)
val state: BleState,
val accelerometerState: AccelerometerState
) : BleView(info) {
class AccelerometerState(
saveHistory: Boolean,
historyInterval: Long
) {
var saveHistory by mutableStateOf(saveHistory)
var historyInterval by mutableStateOf(historyInterval)
}
}
class Beacon(
info: BleInfo,

View File

@ -260,16 +260,40 @@ private fun BleItem(
) {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = when(ble.type){
BleInfo.Type.BEACON -> Icons.Rounded.Nfc
BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
},
contentDescription = null
)
Box {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = when (ble.type) {
BleInfo.Type.BEACON -> Icons.Rounded.Nfc
BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
},
contentDescription = null
)
}
if(ble.recordEnabled){
Surface(
shape = CircleShape,
color = color,
modifier = Modifier.align(Alignment.TopEnd)
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.size(12.dp).padding(2.dp)
) {
}
}
}
}
Column {
@ -281,21 +305,6 @@ 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)

View File

@ -4,6 +4,7 @@ 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.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.common.BleException
@ -26,6 +27,11 @@ class ConnectionContract {
val event: ThermometerContract.Effect.Navigation
) : Event()
data class OnAccelNavigationEvent(
val event: AccelerometerContract.Effect.Navigation
) : Event()
}
sealed class State : ViewState {

View File

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerScreen
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconScreen
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
@ -117,7 +118,11 @@ fun ConnectionScreen(
}
is Ble.Accelerometer -> {
AccelerometerScreen(ble = state.ble)
AccelerometerScreen(ble = state.ble) {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
}
}
}

View File

@ -10,6 +10,7 @@ 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.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
@ -35,6 +36,7 @@ class ConnectionViewModel @Inject constructor(
is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.RefreshBle -> reduce(viewState.value, event)
is ConnectionContract.Event.OnAccelNavigationEvent -> reduce(viewState.value, event)
}
}
@ -74,6 +76,19 @@ class ConnectionViewModel @Inject constructor(
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
when(event.event){
AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnNavigateUp

View File

@ -4,6 +4,7 @@ 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.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
@ -15,12 +16,15 @@ class AccelerometerContract {
sealed class Event : ViewEvent {
object OnShowAccelerometerMeasure : Event()
object OnHideAccelerometerMeasure : Event()
object OnShowAccelerometerAccel : Event()
object OnHideAccelerometerAccel : Event()
object OnShowAccelerometerSpectre : Event()
object OnHideAccelerometerSpectre : Event()
object OnShowAccelerometerHistory : Event()
object OnHideAccelerometerHistory : Event()
data class OnAccelViewModeEdit(
val next: Next
) : Event(){
@ -44,6 +48,9 @@ class AccelerometerContract {
object OnHideWriteBlePreview : Event()
object OnChangePassword : Event()
object OnSaveIntervalEdit : Event()
data class OnBleChanged(
val ble: Ble.Accelerometer,
): Event()
@ -68,6 +75,14 @@ class AccelerometerContract {
val mode: FftViewMode
) : Event()
data class OnSaveHistoryChanged(
val save: Boolean
) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
}
sealed class State : ViewState {
@ -107,7 +122,8 @@ class AccelerometerContract {
sealed class Effect : ViewSideEffect {
object ShowAccelerometerMeasure : Effect()
object ShowAccelerometerAccel : Effect()
object ShowAccelerometerSpectre : Effect()
object ShowAccelerometerHistory : Effect()
object ShowPowerPicker : Effect()
@ -125,6 +141,15 @@ class AccelerometerContract {
object ShowFftAxisEdit : Effect()
object ShowFftModeEdit : Effect()
object HideIntervalPicker : Effect()
object ShowIntervalPicker : Effect()
sealed class Navigation : Effect() {
object NavigateToChangePassword : Navigation()
}
}
}

View File

@ -1,6 +1,5 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
@ -23,22 +22,25 @@ import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftModeEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFrequencyEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelSpectreEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelViewEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerSpectre
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerAccel
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.IntervalEdit
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, POWER, WRITE, HISTORY, ACCEL_MODE_EDIT, SPECTRE_MODE_EDIT, SPECTRE_EDIT, FREQUENCY_EDIT, AXIS_EDIT, FFT_MODE_EDIT
HISTORY, ACCEL, POWER, WRITE, SPECTRE, ACCEL_MODE_EDIT, SPECTRE_MODE_EDIT, SPECTRE_EDIT, FREQUENCY_EDIT, AXIS_EDIT, FFT_MODE_EDIT, INTERVAL_EDIT
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AccelerometerScreen(
ble: Ble.Accelerometer,
onEvent: (AccelerometerContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
@ -57,9 +59,7 @@ fun AccelerometerScreen(
LaunchedEffect(
key1 = bottomDialog.sheetState?.currentValue,
block = {
Log.d("sheet", bottomDialog.sheetState?.currentValue.toString())
if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
Log.d("sheet", "dispose")
bottomDialog.setContent({})
sheetPage = null
}
@ -70,12 +70,12 @@ fun AccelerometerScreen(
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.MEASURE -> launch {
SheetPage.HISTORY -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerMeasure(
AccelerometerHistory(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
@ -86,12 +86,28 @@ fun AccelerometerScreen(
}
}
}
SheetPage.HISTORY -> launch {
SheetPage.ACCEL -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerHistory(
AccelerometerAccel(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
fftMode = currentState.fftViewMode,
frequency = currentState.fftFrequency,
accelScale = currentState.accelScale
)
}
}
}
SheetPage.SPECTRE -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerSpectre(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
@ -221,9 +237,27 @@ fun AccelerometerScreen(
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
IntervalEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
null -> {
bottomDialog.hide()
}
}
}
@ -238,10 +272,10 @@ fun AccelerometerScreen(
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is AccelerometerContract.Effect.ShowAccelerometerMeasure -> launch {
is AccelerometerContract.Effect.ShowAccelerometerAccel -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.MEASURE
sheetPage = SheetPage.ACCEL
}
is AccelerometerContract.Effect.HidePowerPicker -> launch {
sheetPage = null
@ -261,10 +295,10 @@ fun AccelerometerScreen(
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.ShowAccelerometerHistory -> launch {
is AccelerometerContract.Effect.ShowAccelerometerSpectre -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY
sheetPage = SheetPage.SPECTRE
}
is AccelerometerContract.Effect.ShowAccelEdit -> launch {
sheetPage = null
@ -294,6 +328,25 @@ fun AccelerometerScreen(
delay(100)
sheetPage = SheetPage.FFT_MODE_EDIT
}
AccelerometerContract.Effect.ShowAccelerometerHistory -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY
}
AccelerometerContract.Effect.HideIntervalPicker -> {
sheetPage = null
delay(100)
}
AccelerometerContract.Effect.ShowIntervalPicker -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL_EDIT
}
is AccelerometerContract.Effect.Navigation -> {
onEvent(it)
}
}
}.launchIn(this)
}

View File

@ -1,14 +1,14 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import android.util.Log
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
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.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
@ -30,8 +30,8 @@ class AccelerometerViewModel @Inject constructor(
override fun handleEvents(event: AccelerometerContract.Event) {
when(event){
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.OnHideAccelerometerAccel -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerAccel -> 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)
@ -48,9 +48,62 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.Event.OnFftModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftAxisEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalEdit
) {
setEffect {
AccelerometerContract.Effect.ShowIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.historyInterval = event.interval
}
setEffect {
AccelerometerContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnChangePassword
) {
setEffect {
AccelerometerContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveHistoryChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.saveHistory = event.save
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftModeEdit
@ -152,7 +205,7 @@ class AccelerometerViewModel @Inject constructor(
) {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerHistory
AccelerometerContract.Effect.ShowAccelerometerSpectre
}
}
@ -164,6 +217,26 @@ class AccelerometerViewModel @Inject constructor(
}
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(
@ -197,8 +270,10 @@ class AccelerometerViewModel @Inject constructor(
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
)
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
saveHistory = if(newBle.accelerometerState.saveHistory == state.origin.accelerometerState.saveHistory) null else newBle.accelerometerState.saveHistory,
historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
)
setState {
state.copy(
@ -274,7 +349,7 @@ class AccelerometerViewModel @Inject constructor(
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerMeasure
event: AccelerometerContract.Event.OnHideAccelerometerAccel
) {
@ -283,13 +358,13 @@ class AccelerometerViewModel @Inject constructor(
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerMeasure
event: AccelerometerContract.Event.OnShowAccelerometerAccel
) {
viewModelScope.launch {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerMeasure
AccelerometerContract.Effect.ShowAccelerometerAccel
}
}
@ -308,7 +383,8 @@ class AccelerometerViewModel @Inject constructor(
state.copy(
origin = Ble.Accelerometer(
info = event.ble.info,
state = event.ble.state
state = event.ble.state,
accelerometerState = state.origin.accelerometerState
)
)
}
@ -361,6 +437,10 @@ class AccelerometerViewModel @Inject constructor(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistory = request.writeRequest.saveHistory
?: currentState.origin.accelerometerState.saveHistory
)
)

View File

@ -13,9 +13,7 @@ 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
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.AccelViewMode.*
@ -90,7 +88,7 @@ fun AccelViewEdit(
onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
when(next){
AccelerometerContract.Event.OnAccelViewModeEdit.Next.ACCEL -> {
onEvent(AccelerometerContract.Event.OnShowAccelerometerMeasure)
onEvent(AccelerometerContract.Event.OnShowAccelerometerAccel)
}
AccelerometerContract.Event.OnAccelViewModeEdit.Next.SPECTRE -> {
onEvent(AccelerometerContract.Event.OnSpectreEdit)

View File

@ -0,0 +1,499 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import android.util.Log
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.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 com.patrykandpatrick.vico.core.entry.FloatEntry
import kotlinx.coroutines.Job
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.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
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 AccelerometerSpectre(
ble: BleInfo,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale
) {
val viewModel = hiltViewModel<AccelerometerSpectreViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial, accelMode, fftAxis, fftMode, frequency) {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
}
DisposableEffect(key1 = "ble", effect = {
onDispose {
Log.d("history", "dispose")
viewModel.setEvent(AccelerometerSpectreContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerSpectreContract.State.Display -> {
if (state.previousHistory !== null) {
"${fftMode.localized} (${state.previousHistory.size})"
}else {
fftMode.localized
}
}
AccelerometerSpectreContract.State.Exception -> fftMode.localized
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = title,
style = MaterialTheme.typography.titleLarge
)
if(state is AccelerometerSpectreContract.State.Display) {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
progress = progressAnimation,
)
}
else -> {}
}
}
}
IconButton(
onClick = {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
},
enabled = when(state){
is AccelerometerSpectreContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerSpectreContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerSpectreContract.State.Display -> Display(state = state)
AccelerometerSpectreContract.State.Exception -> Exception()
}
}
}
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun Display(
state: AccelerometerSpectreContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp
)
val scrollState = rememberChartScrollState()
Chart(
diffAnimationSpec = tween(0),
isZoomEnabled = true,
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize()
)
}
} else {
when (state.loadingHistoryState) {
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)
)
}
else -> {}
}
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerSpectreContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
}
sealed class State : ViewState {
data class Display(
val previousHistory : List<Ble.Accelerometer.MeasurePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.MeasurePoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerSpectreViewModel @Inject constructor(
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
) : BaseViewModel<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
private var job: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
override fun handleEvents(event: AccelerometerSpectreContract.Event) {
when(event){
is AccelerometerSpectreContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerSpectreContract.State.Display) {
//if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when(state.loadingHistoryState){
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
} else {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerSpectreContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerSpectreContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnRefreshHistory
) {
/*viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}*/
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.StopMeasure
) {
job?.cancel()
}
}

View File

@ -1,6 +1,5 @@
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.*
@ -45,7 +44,7 @@ import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
@Composable
fun AccelerometerMeasure(
fun AccelerometerAccel(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
@ -54,20 +53,15 @@ fun AccelerometerMeasure(
frequency: FftFrequency
) {
val viewModel = hiltViewModel<AccelerometerMeasureViewModel>()
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value
/*LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial))
}*/
viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
viewModel.setEvent(AccelerometerAccelContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
DisposableEffect(key1 = "ble", effect = {
onDispose {
Log.d("measure", "dispose")
viewModel.setEvent(AccelerometerMeasureContract.Event.StopMeasure)
viewModel.setEvent(AccelerometerAccelContract.Event.StopMeasure)
}
})
@ -89,7 +83,7 @@ fun AccelerometerMeasure(
IconButton(
onClick = {
viewModel.setEvent(AccelerometerMeasureContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
},
enabled = true
) {
@ -106,8 +100,8 @@ fun AccelerometerMeasure(
Box(modifier = Modifier) {
when (state) {
is AccelerometerMeasureContract.State.Display -> Display(state = state)
AccelerometerMeasureContract.State.Exception -> Exception()
is AccelerometerAccelContract.State.Display -> Display(state = state)
is AccelerometerAccelContract.State.Exception -> Exception()
}
}
@ -119,7 +113,7 @@ fun AccelerometerMeasure(
@Composable
fun Display(
state: AccelerometerMeasureContract.State.Display
state: AccelerometerAccelContract.State.Display
) {
Box(modifier = Modifier
@ -255,7 +249,7 @@ private fun Exception(
}
class AccelerometerMeasureContract {
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
@ -300,29 +294,29 @@ class AccelerometerMeasureContract {
@HiltViewModel
class AccelerometerMeasureViewModel @Inject constructor(
class AccelerometerAccelViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerMeasureContract.State, AccelerometerMeasureContract.Event, AccelerometerMeasureContract.Effect>() {
) : BaseViewModel<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerMeasureContract.State.Display(
override fun setInitialState() = AccelerometerAccelContract.State.Display(
emptyList()
)
override fun handleEvents(event: AccelerometerMeasureContract.Event) {
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerMeasureContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerMeasureContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerMeasureContract.Event.StopMeasure -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.StopMeasure
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.StopMeasure
) {
@ -330,14 +324,14 @@ class AccelerometerMeasureViewModel @Inject constructor(
measureJob = null
setState {
AccelerometerMeasureContract.State.Display(emptyList())
AccelerometerAccelContract.State.Display(emptyList())
}
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.OnStart
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnStart
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, false)
@ -345,8 +339,8 @@ class AccelerometerMeasureViewModel @Inject constructor(
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.OnRefreshHistory
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefreshHistory
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, true)
}
@ -367,7 +361,7 @@ class AccelerometerMeasureViewModel @Inject constructor(
measureJob = viewModelScope.launch {
setState {
AccelerometerMeasureContract.State.Display(emptyList())
AccelerometerAccelContract.State.Display(emptyList())
}
getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency).onEach {
@ -375,7 +369,7 @@ class AccelerometerMeasureViewModel @Inject constructor(
onSuccess = {
setState {
when (this) {
is AccelerometerMeasureContract.State.Display -> {
is AccelerometerAccelContract.State.Display -> {
val dataList = this.measureHistory.toMutableList().apply {
add(
Accelerate(
@ -385,18 +379,18 @@ class AccelerometerMeasureViewModel @Inject constructor(
)
)
}.takeLast(10)
AccelerometerMeasureContract.State.Display(dataList)
AccelerometerAccelContract.State.Display(dataList)
}
AccelerometerMeasureContract.State.Exception -> {
AccelerometerMeasureContract.State.Display(listOf(it))
AccelerometerAccelContract.State.Exception -> {
AccelerometerAccelContract.State.Display(listOf(it))
}
}
}
},
onFailure = {
setState {
AccelerometerMeasureContract.State.Exception
AccelerometerAccelContract.State.Exception
}
}
)

View File

@ -1,9 +1,9 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import android.util.Log
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.*
@ -12,7 +12,6 @@ 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.core.entry.ChartEntryModelProducer
@ -28,60 +27,68 @@ 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.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
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.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.extension.sumByFloat
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
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.app.ui.screen.inspection.thermometer.view.TemperatureEntry
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.TemperatureHistoryContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.formatter
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.Accelerate
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
import llc.arma.ble.domain.usecase.GetAccelerometerHistoryBySerial
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import java.util.Date
class AccelerometerEntry(
val frequency: Long,
class AccelEntry(
val localDate: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
override fun withY(y: Float) = AccelEntry(localDate, x, y)
}
@Composable
fun AccelerometerHistory(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale
frequency: FftFrequency
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial, accelMode, fftAxis, fftMode, frequency) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
}
DisposableEffect(key1 = "ble", effect = {
DisposableEffect("ble") {
onDispose {
Log.d("history", "dispose")
viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure)
}
})
}
Column(
modifier = Modifier.fillMaxHeight(0.9f)
@ -94,69 +101,26 @@ fun AccelerometerHistory(
val title = when(state){
is AccelerometerHistoryContract.State.Display -> {
if (state.previousHistory !== null) {
"${fftMode.localized} (${state.previousHistory.size})"
}else {
fftMode.localized
}
}
AccelerometerHistoryContract.State.Exception -> fftMode.localized
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = title,
style = MaterialTheme.typography.titleLarge
)
if(state is AccelerometerHistoryContract.State.Display) {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
progress = progressAnimation,
)
}
else -> {}
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.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
},
enabled = when(state){
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> true
}
@ -175,7 +139,7 @@ fun AccelerometerHistory(
when (state) {
is AccelerometerHistoryContract.State.Display -> Display(state = state)
AccelerometerHistoryContract.State.Exception -> Exception()
is AccelerometerHistoryContract.State.Exception -> Exception()
}
}
@ -184,13 +148,6 @@ fun AccelerometerHistory(
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun Display(
@ -202,98 +159,93 @@ fun Display(
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
when (state.loadingHistoryState) {
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
is ProgressState.Finished -> {
if(data != null){
if(state.loadingHistoryState.data.isEmpty()){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
} else {
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
producer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
AccelEntry(measurePoint.frequency, index.toFloat(), measurePoint.value )
})
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelEntry)
?.localDate
?.let { formatter.format(Date(it)) }
.orEmpty()
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp
)
val lineChart = lineChart()
val scrollState = rememberChartScrollState()
Chart(
diffAnimationSpec = tween(0),
isZoomEnabled = true,
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize()
)
}
} else {
when (state.loadingHistoryState) {
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
Chart(
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize(),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
}
else -> {}
}
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
), label = ""
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
private fun Exception(
) {
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)
@ -319,20 +271,20 @@ class AccelerometerHistoryContract {
data class OnStart(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
val frequency: FftFrequency
) : Event()
}
@ -340,7 +292,6 @@ class AccelerometerHistoryContract {
sealed class State : ViewState {
data class Display(
val previousHistory : List<Ble.Accelerometer.MeasurePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.MeasurePoint>>
) : State()
@ -358,15 +309,15 @@ class AccelerometerHistoryContract {
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
private var job: Job? = null
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerHistoryContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
ProgressState.Indeterminate
)
override fun handleEvents(event: AccelerometerHistoryContract.Event) {
@ -377,6 +328,21 @@ class AccelerometerHistoryViewModel @Inject constructor(
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerHistoryContract.State.Exception
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnStart
@ -386,73 +352,36 @@ class AccelerometerHistoryViewModel @Inject constructor(
if(state is AccelerometerHistoryContract.State.Display) {
//if(lastSerial != event.serial) {
if(lastSerial != event.serial) {
lastSerial = event.serial
lastSerial = event.serial
setState {
AccelerometerHistoryContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when(state.loadingHistoryState){
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
} else {
setState {
AccelerometerHistoryContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerHistoryContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
@ -461,20 +390,16 @@ class AccelerometerHistoryViewModel @Inject constructor(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnRefreshHistory
) {
/*viewModelScope.launch {
viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
@ -489,14 +414,7 @@ class AccelerometerHistoryViewModel @Inject constructor(
)
}.launchIn(this)
}*/
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.StopMeasure
) {
job?.cancel()
}
}
}

View File

@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
@Composable
@ -89,6 +90,123 @@ fun DisplayState(
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
}
Switch(
checked = ble.accelerometerState.saveHistory,
onCheckedChange = {
onEvent(AccelerometerContract.Event.OnSaveHistoryChanged(it))
}
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерений"
)
val hours = ble.accelerometerState.historyInterval / 1000 / 60 / 60
val minutes = (ble.accelerometerState.historyInterval - ( hours * 1000 * 60 * 60 )) / 1000 / 60
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин."
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
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 = "График измерений"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
@ -159,6 +277,42 @@ fun DisplayState(
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
}
)

View File

@ -0,0 +1,202 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
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 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 IntervalEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelerometerState.historyInterval) {
mutableStateOf((state.accelerometerState.historyInterval / 1000 / 60 / 60).toInt())
}
val maxInterval = 240
if(value > maxInterval){
value = maxInterval
}
if(value < 1){
value = 1
}
val maxHours = maxInterval
val maxDays = maxInterval / 24
val dayValue = value / 24
val hourValue = value - (24 * dayValue)
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Интервал измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
NumberPicker(
range = -1..maxDays,
value = dayValue,
onValueChanged = { value = (it * 24) + hourValue }
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Дни")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxHours,
value = hourValue,
onValueChanged = {
value = it + (dayValue * 24)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Часы")
}
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.OnSaveIntervalChanged(
value.toLong() * 1000 * 60 * 60
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}
@Composable
fun NumberPicker(
modifier: Modifier = Modifier,
range: IntRange,
value: Int,
onValueChanged: (Int) -> Unit
) {
LaunchedEffect(range){
if(value > range.last){
onValueChanged(range.last)
}
if(value < range.first){
onValueChanged(range.first)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
){
FilledIconButton(
onClick = {
if(value < range.last) onValueChanged(value + 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = null
)
}
Spacer(modifier = Modifier.height(36.dp))
AnimatedContent(
targetState = value,
transitionSpec = {
if (targetState > initialState) {
(slideInVertically { height -> height } + fadeIn()).togetherWith(
slideOutVertically { height -> -height } + fadeOut())
} else {
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
slideOutVertically { height -> height } + fadeOut())
}.using(
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(
style = MaterialTheme.typography.displaySmall,
text = "$targetCount"
)
}
Spacer(modifier = Modifier.height(36.dp))
FilledIconButton(
onClick = {
if(value > range.first) onValueChanged(value - 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}

View File

@ -44,7 +44,7 @@ fun Write(
when (state) {
is AccelerometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null) {
if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) {
state.writeRequest.tx?.let {
Box(
@ -81,6 +81,80 @@ fun Write(
}
}
state.writeRequest.saveHistory?.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
)
}
}
}
}
state.writeRequest.historyInterval?.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 / 1000 / 60 / 60} ч."
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(

View File

@ -204,7 +204,7 @@ fun Display(
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
), label = ""
)
CircularProgressIndicator(
@ -221,9 +221,7 @@ fun Display(
}
@Composable
private fun Exception(
) {
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)

View File

@ -106,7 +106,7 @@ fun Write(
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName}"
text = it.localizedName
)
}

View File

@ -126,6 +126,11 @@ class BleRepositoryImpl @Inject constructor(
private val app: Application
) : BleRepository {
private val ScanResult.timerEnabled: Boolean
get() {
return scanRecord?.manufacturerSpecificData?.get(89)?.get(2) == 1.toByte()
}
private val ScanResult.info: BleInfo
get() {
return BleInfo(
@ -135,15 +140,11 @@ class BleRepositoryImpl @Inject constructor(
rssi = rssi,
type = type,
scanTime = timestampNanos / 1_000_000,
tx = scanRecord?.txPowerLevel ?: 0
tx = scanRecord?.txPowerLevel ?: 0,
recordEnabled = timerEnabled
)
}
private val ScanResult.timerEnabled: Boolean
get() {
return scanRecord?.manufacturerSpecificData?.get(89)?.get(2) == 1.toByte()
}
private val ScanResult.batteryLevel: Int?
get() {
return scanRecord?.manufacturerSpecificData?.get(89)?.get(1)
@ -300,6 +301,24 @@ class BleRepositoryImpl @Inject constructor(
return when(result.info.type) {
BleInfo.Type.ACCELEROMETER -> {
val tState = suspendCancellableCoroutine {
CoroutineScope(Dispatchers.IO).launch {
it.resume(readAccelState(result))
}
}.fold(
onFailure = {
return Result.failure(it)
},
onSuccess = {
it
}
)
Result.success(
flow {
@ -329,7 +348,8 @@ class BleRepositoryImpl @Inject constructor(
4 -> Ble.BleState.TX.PLUS_4
else -> Ble.BleState.TX.ZERO
}
)
),
accelerometerState = tState
)
)
@ -457,10 +477,6 @@ class BleRepositoryImpl @Inject constructor(
)
}
BleInfo.Type.ACCELEROMETER -> {
TODO()
}
}
}
@ -497,6 +513,26 @@ class BleRepositoryImpl @Inject constructor(
}
private suspend fun readAccelState(
record: ScanResult
): Result<Ble.Accelerometer.AccelerometerState, BleException> {
val history = readHistoryInterval(record).fold(
onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
)
return Result.success(
Ble.Accelerometer.AccelerometerState(
saveHistory = record.timerEnabled,
historyInterval = history
)
)
}
@OptIn(ExperimentalUnsignedTypes::class)
private suspend fun readTemperature(
record: ScanResult
@ -620,7 +656,45 @@ class BleRepositoryImpl @Inject constructor(
if (checkPermission()) {
gatt = it.connectGatt(app, false, ReadHistoryCallback(app) {
gatt = it.connectGatt(app, false, ReadTemperatureHistoryCallback(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 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)
}

View File

@ -165,7 +165,7 @@ class ReadAccelerometerCallback(
Log.d("accel", "notification")
val data = value.toList().chunked(2).map {
it.toByteArray().get2byteShortAt(0)
it.toByteArray().get2byteShortAt()
}
onResult(

View File

@ -0,0 +1,337 @@
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() {
enum class Property {
DATA_SIZE, PACKAGE
}
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)
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerHistoryReadUUID)?.let {
if (checkPermission()) {
readProperty = Property.DATA_SIZE
gatt.writeCharacteristic(it, byteArrayOf(2))
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
private var lastMeasureSystemTime: Long? = null
private var bleMeasureInterval: Long? = null
private var bleRealTime: Long? = null
private var bleLastMeasureTime: Long? = null
private val resultTemperaturePackage: MutableList<Float> = mutableListOf()
var expectedDataSize: Int? = null
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
){
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 = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) * 1_000)
val temperatureDataArray = value.toUByteArray().asList().subList(16, value.size)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
it.toUByteArray().toByteArray().get2byteShortAt().toFloat()
}.toMutableList()
)
val nextPackageDataCount = value.get2byteUIntAt(2)
expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat())))
onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.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(
resultTemperaturePackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = lastMeasureSystemTime!! - (((resultTemperaturePackage.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)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
it.toUByteArray().toByteArray().get2byteShortAt().toFloat()
}
)
onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.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(
resultTemperaturePackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = lastMeasureSystemTime!! - (((resultTemperaturePackage.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

@ -23,13 +23,12 @@ import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.util.UUID
fun ByteArray.get2byteShortAt(idx: Int): Int {
fun ByteArray.get2byteShortAt(): Int {
val shorts = ShortArray(1)
ByteBuffer.wrap(this).order(LITTLE_ENDIAN).asShortBuffer()[shorts]
return shorts[0].toInt()//(this[0].toInt() + (this[1].toInt() shl 8)).toShort()
return shorts[0].toInt()
}
class ReadAccelerometerSpectreCallback(
private val app: Application,
private val accelScale: AccelScale,
@ -67,11 +66,6 @@ class ReadAccelerometerSpectreCallback(
) {
super.onConnectionStateChange(gatt, status, newState)
Log.d("spectre", "onConnectionStateChange")
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
@ -98,10 +92,9 @@ class ReadAccelerometerSpectreCallback(
status: Int
) {
super.onServicesDiscovered(gatt, status)
Log.d("spectre", "onServicesDiscovered")
if(status == BluetoothGatt.GATT_SUCCESS){
enableNotifications(gatt)
}
}
@ -148,7 +141,7 @@ class ReadAccelerometerSpectreCallback(
private val resultAccelerometerPackage: MutableList<Float> = mutableListOf()
var expectedDataSize: Int? = null
private var expectedDataSize: Int? = null
@Deprecated("Deprecated in Java")
override fun onCharacteristicRead(
@ -189,7 +182,7 @@ class ReadAccelerometerSpectreCallback(
){
if(characteristic.uuid == accelerometerReadUUID) {
Log.d("spectre", "changed")
readProperty = Property.DATA_SIZE
gatt.getService(serviceUUID).getCharacteristic(accelerometerHistoryReadUUID)?.let {
gatt.writeCharacteristic(it, byteArrayOf(2))
@ -215,8 +208,6 @@ class ReadAccelerometerSpectreCallback(
status: Int
){
Log.d("spectre", "onCharacteristicRead")
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){
Property.DATA_SIZE -> {
@ -257,7 +248,7 @@ class ReadAccelerometerSpectreCallback(
resultAccelerometerPackage.addAll(
accelerometerDataArray.chunked(2).map {
it.toByteArray().get2byteShortAt(0).toFloat()
it.toByteArray().get2byteShortAt().toFloat()
}.toMutableList()
)
@ -309,7 +300,7 @@ class ReadAccelerometerSpectreCallback(
resultAccelerometerPackage.addAll(
temperatureDataArray.chunked(2).map {
it.toByteArray().get2byteShortAt(0).toFloat()
it.toByteArray().get2byteShortAt().toFloat()
}
)
@ -365,8 +356,6 @@ class ReadAccelerometerSpectreCallback(
) {
super.onCharacteristicWrite(gatt, characteristic, status)
Log.d("spectre", "request written $readProperty")
if(readProperty !== null) {
if (status == BluetoothGatt.GATT_SUCCESS) {
@ -390,29 +379,19 @@ class ReadAccelerometerSpectreCallback(
}
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
Log.d("spectre", "mtu $mtu")
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
Log.d("spectre", "descriptor written")
super.onDescriptorWrite(gatt, descriptor, status)
start(gatt)
}
private fun start(
gatt: BluetoothGatt,
){
Log.d("spectre", "start")
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {

View File

@ -13,17 +13,16 @@ 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 java.util.stream.Collectors
enum class Property {
DATA_SIZE, PACKAGE
}
class ReadHistoryCallback(
class ReadTemperatureHistoryCallback(
private val app: Application,
private val onResult: (Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
enum class Property {
DATA_SIZE, PACKAGE
}
private fun ByteArray.get4byteUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or
((this[idx + 2].toUInt() and 0xFFu) shl 16) or

View File

@ -64,10 +64,43 @@ class WriteAccelerometerCallback(
status: Int
){
if(request.tx != null) {
if(request.tx != null || request.saveHistory != null) {
fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
(3 downTo 0).map {
(this shr (it * Byte.SIZE_BITS)).toByte()
}.toByteArray()
var uuid: Pair<UUID, ByteArray>? = null
uuid = request.historyInterval?.let {
this.request = request.copy(
historyInterval = null
)
Pair(
intervalWriteUUID,
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
)
}
uuid = request.saveHistory?.let {
this.request = request.copy(
saveHistory = null
)
Pair(
saveEnabledWriteUUID,
mutableListOf<Byte>(4).apply {
add(if (it) 1 else 0)
}.toByteArray()
)
} ?: uuid
uuid = request.tx?.let {
this.request = request.copy(
@ -143,8 +176,6 @@ class WriteAccelerometerCallback(
status: Int
) {
Log.d("beacon", "onCharacteristicWrite $status")
super.onCharacteristicWrite(gatt, characteristic, status)
if(checkPermission()) {

View File

@ -34,8 +34,6 @@ class WriteThermometerCallback(
) {
super.onConnectionStateChange(gatt, status, newState)
Log.d("th", "onConnectionStateChange $status $newState")
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
@ -60,7 +58,6 @@ class WriteThermometerCallback(
gatt: BluetoothGatt,
status: Int
) {
Log.d("th", "onServicesDiscovered $status")
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)
@ -183,8 +180,6 @@ class WriteThermometerCallback(
status: Int
) {
Log.d("th", "onCharacteristicWrite $status")
super.onCharacteristicWrite(gatt, characteristic, status)
if(checkPermission()) {

View File

@ -6,11 +6,14 @@ sealed class Ble(
class Accelerometer(
info: BleInfo,
val state: BleState
val state: BleState,
val accelerometerState: AccelerometerState
): Ble(info) {
data class WriteRequest(
val tx: BleState.TX?,
val saveHistory: Boolean?,
val historyInterval: Long?
)
class MeasurePoint (
@ -18,6 +21,11 @@ sealed class Ble(
val value: Float
)
data class AccelerometerState(
val saveHistory: Boolean,
val historyInterval: Long
)
}
class Beacon(

View File

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

View File

@ -50,4 +50,6 @@ interface BleRepository {
frequency: FftFrequency
): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, 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)
}
}