rotations save

This commit is contained in:
Vineyro 2024-07-05 17:00:32 +07:00
parent 55b6807081
commit 549b625820
37 changed files with 1514 additions and 129 deletions

View File

@ -96,6 +96,10 @@ dependencies {
implementation "com.patrykandpatrick.vico:compose:1.7.1" implementation "com.patrykandpatrick.vico:compose:1.7.1"
implementation "com.patrykandpatrick.vico:compose-m3:1.7.1" implementation "com.patrykandpatrick.vico:compose-m3:1.7.1"
implementation "androidx.room:room-runtime:2.6.1"
kapt("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation files('libs/poishadow-all.jar') implementation files('libs/poishadow-all.jar')
} }

View File

@ -0,0 +1,30 @@
package llc.arma.ble.app.framework.di
import android.app.Application
import androidx.room.Room
import androidx.room.RoomDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import llc.arma.ble.data.db.AppDatabase
import llc.arma.ble.data.db.RotationsDao
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
@Provides
@Singleton
fun provideDatabase(app: Application): AppDatabase {
return Room.databaseBuilder(app, AppDatabase::class.java, "app-database").build()
}
@Provides
@Singleton
fun provideRotationsDao(db: AppDatabase): RotationsDao {
return db.getRotationsDao()
}
}

View File

@ -4,11 +4,13 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import llc.arma.ble.data.BleRepositoryImpl import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.data.EmailRepositoryImpl import llc.arma.ble.data.repository.EmailRepositoryImpl
import llc.arma.ble.data.XlsxRepositoryImpl import llc.arma.ble.data.repository.RotationsRepositoryImpl
import llc.arma.ble.data.repository.XlsxRepositoryImpl
import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.repository.EmailRepository import llc.arma.ble.domain.repository.EmailRepository
import llc.arma.ble.domain.repository.RotationsRepository
import llc.arma.ble.domain.repository.XlsxRepository import llc.arma.ble.domain.repository.XlsxRepository
@Module @Module
@ -21,6 +23,9 @@ interface RepositoryBinding {
@Binds @Binds
fun bindEmailRepository(repository: EmailRepositoryImpl): EmailRepository fun bindEmailRepository(repository: EmailRepositoryImpl): EmailRepository
@Binds
fun bindRotationsRepository(repository: RotationsRepositoryImpl): RotationsRepository
@Binds @Binds
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository

View File

@ -7,15 +7,19 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.SurfaceView
import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
@ -28,8 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat.startActivityForResult
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -231,19 +234,20 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun OnLifecycleEvent(onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit) { fun Camera(){
val eventHandler = rememberUpdatedState(onEvent)
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
DisposableEffect(lifecycleOwner.value) { AndroidView(
val lifecycle = lifecycleOwner.value.lifecycle factory = {
val observer = LifecycleEventObserver { owner, event -> val cameraManager = it.getSystemService(CameraManager::class.java)
eventHandler.value(owner, event)
}
lifecycle.addObserver(observer) cameraManager.cameraIdList.forEach {
onDispose {
lifecycle.removeObserver(observer) println("$it is front: ${cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT}")
}
return@AndroidView SurfaceView(it)
} }
} )
} }

View File

@ -66,6 +66,10 @@ class ConnectionContract {
val serial: String val serial: String
) : Navigation() ) : Navigation()
data class NavigateToRotationsStatistic(
val serial: String
) : Navigation()
} }
sealed class InnerNavigation : Effect() { sealed class InnerNavigation : Effect() {

View File

@ -184,7 +184,9 @@ fun ConnectionScreen(
onDismiss = { onDismiss = {
innerScreen = null innerScreen = null
} }
) ){
onNavigationEvent(ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic(it.ble.serial))
}
} }

View File

@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.CloudUpload import androidx.compose.material.icons.rounded.CloudUpload
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.TableView
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
@ -71,6 +72,7 @@ class AccelEntry(
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccelerometerHistory( fun AccelerometerHistory(
ble: BleInfo, ble: BleInfo,
@ -79,7 +81,8 @@ fun AccelerometerHistory(
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency, frequency: FftFrequency,
onDismiss: (() -> Unit)? = null onDismiss: (() -> Unit)? = null,
onShowStatistic: () -> Unit,
) { ) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>() val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
@ -95,74 +98,85 @@ fun AccelerometerHistory(
} }
}*/ }*/
Column( Column() {
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row( TopAppBar(
modifier = Modifier.padding(horizontal = 12.dp), navigationIcon = {
verticalAlignment = Alignment.CenterVertically onDismiss?.let {
) {
onDismiss?.let { IconButton(onClick = it) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
IconButton(onClick = it) { }
},
title = {
val title = when(state){
is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> accelMode.localized
is ProgressState.Progress -> accelMode.localized
}
}
AccelerometerHistoryContract.State.Exception -> accelMode.localized
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = onShowStatistic,
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false
}
) {
Icon( Icon(
imageVector = Icons.Rounded.ArrowBack, imageVector = Icons.Rounded.TableView,
contentDescription = null contentDescription = null
) )
} }
} IconButton(
onClick = {
val title = when(state){ viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport)
is AccelerometerHistoryContract.State.Display -> { },
when (state.loadingHistoryState) { enabled = when(state){
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})" is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
is ProgressState.Indeterminate -> accelMode.localized AccelerometerHistoryContract.State.Exception -> false
is ProgressState.Progress -> accelMode.localized
} }
) {
Icon(
imageVector = Icons.Rounded.CloudUpload,
contentDescription = null
)
} }
AccelerometerHistoryContract.State.Exception -> accelMode.localized
}
Text( IconButton(
modifier = Modifier.weight(1f), onClick = {
text = title, viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
style = MaterialTheme.typography.titleLarge },
) enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
IconButton( AccelerometerHistoryContract.State.Exception -> true
onClick = { }
viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport) ) {
}, Icon(
enabled = when(state){ imageVector = Icons.Rounded.Refresh,
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished contentDescription = null
AccelerometerHistoryContract.State.Exception -> false )
} }
) {
Icon(
imageVector = Icons.Rounded.CloudUpload,
contentDescription = null
)
} }
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
},
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)) Spacer(modifier = Modifier.height(16.dp))

View File

@ -15,6 +15,10 @@ import llc.arma.ble.app.ui.screen.connection.ConnectionContract
import llc.arma.ble.app.ui.screen.connection.ConnectionScreen import llc.arma.ble.app.ui.screen.connection.ConnectionScreen
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteContract
import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteScreen
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticContract
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticScreen
@Composable @Composable
fun MainScreen() { fun MainScreen() {
@ -49,7 +53,10 @@ fun MainScreen() {
onNavigationEvent = { onNavigationEvent = {
when(it){ when(it){
ConnectionContract.Effect.Navigation.NavigateUp -> controller.navigateUp() ConnectionContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
is ConnectionContract.Effect.Navigation.NavigateToChangePassword -> controller.navigate("change_password/${it.serial}") is ConnectionContract.Effect.Navigation.NavigateToChangePassword ->
controller.navigate("change_password/${it.serial}")
is ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic ->
controller.navigate("rotation_statistic/${it.serial}")
} }
} }
) )
@ -57,6 +64,36 @@ fun MainScreen() {
} }
) )
composable(
route = "rotation_statistic/{serial}",
content = {
RotationStatisticScreen {
when(it){
is RotationStatisticContract.Effect.Navigation.NavigateToDelete -> {
controller.navigate("rotation_delete/${it.serial}")
}
RotationStatisticContract.Effect.Navigation.NavigateUp -> {
controller.navigateUp()
}
}
}
}
)
dialog(
route = "rotation_delete/{serial}",
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
content = {
RotationDeleteScreen {
when(it){
RotationDeleteContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
}
}
}
)
dialog( dialog(
route = "change_password/{serial}", route = "change_password/{serial}",
dialogProperties = DialogProperties(usePlatformDefaultWidth = false), dialogProperties = DialogProperties(usePlatformDefaultWidth = false),

View File

@ -0,0 +1,30 @@
package llc.arma.ble.app.ui.screen.rotation.delete
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.Rotation
class RotationDeleteContract {
sealed class Event : ViewEvent {
object OnNavigateUp : Event()
object OnDelete : Event()
}
object State : ViewState
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect(){
object NavigateUp : Navigation()
}
}
}

View File

@ -0,0 +1,118 @@
package llc.arma.ble.app.ui.screen.rotation.delete
import android.icu.text.SimpleDateFormat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material3.Card
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticContract
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.math.pow
import kotlin.math.roundToInt
@Composable
fun RotationDeleteScreen(
onNavigation: (RotationDeleteContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<RotationDeleteViewModel>()
LaunchedEffect(Unit) {
viewModel.effect.onEach {
when(it){
is RotationDeleteContract.Effect.Navigation -> onNavigation(it)
}
}.launchIn(this)
}
Surface(
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
style = MaterialTheme.typography.titleLarge,
text = "Удалить сохраненные записи?"
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
modifier = Modifier
.align(Alignment.End)
) {
TextButton(
onClick = {
viewModel.setEvent(
RotationDeleteContract.Event.OnNavigateUp
)
}
) {
Text(text = "Отмена")
}
Button(
onClick = {
viewModel.setEvent(
RotationDeleteContract.Event.OnDelete
)
}
) {
Text(text = "Удалить")
}
}
}
}
}

View File

@ -0,0 +1,67 @@
package llc.arma.ble.app.ui.screen.rotation.delete
import android.icu.util.GregorianCalendar
import android.util.Log
import androidx.lifecycle.SavedStateHandle
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.domain.usecase.DeleteRotationsBySerial
import llc.arma.ble.domain.usecase.GetRotationsBySerial
import llc.arma.ble.domain.usecase.GetWheelRadiusBySerial
import llc.arma.ble.domain.usecase.SetWheelRadius
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.absoluteValue
@HiltViewModel
class RotationDeleteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val deleteRotationsBySerial: DeleteRotationsBySerial
) : BaseViewModel<RotationDeleteContract.State, RotationDeleteContract.Event, RotationDeleteContract.Effect>() {
override fun setInitialState() = RotationDeleteContract.State
override fun handleEvents(event: RotationDeleteContract.Event) {
when(event){
is RotationDeleteContract.Event.OnDelete -> reduce(viewState.value, event)
is RotationDeleteContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: RotationDeleteContract.State,
event: RotationDeleteContract.Event.OnNavigateUp
) {
setEffect {
RotationDeleteContract.Effect.Navigation.NavigateUp
}
}
private fun reduce(
state: RotationDeleteContract.State,
event: RotationDeleteContract.Event.OnDelete
) {
viewModelScope.launch {
deleteRotationsBySerial(savedStateHandle.get<String>("serial")!!)
}
setEffect {
RotationDeleteContract.Effect.Navigation.NavigateUp
}
}
}

View File

@ -0,0 +1,69 @@
package llc.arma.ble.app.ui.screen.rotation.statistic
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.Rotation
class RotationStatisticContract {
sealed class Event : ViewEvent {
object OnNavigateUp : Event()
object OnNavigateToDelete : Event()
data class OnRadiusChanged(
val radius: Long?
): Event()
data class OnStartDateChanged(
val start: Long?
): Event()
data class OnStopDateChanged(
val stop: Long?
): Event()
}
sealed class State : ViewState {
object Loading : State()
data class Display(
val rotations: List<Rotation>,
val ranges: List<RotationRange>,
val radius: Long?,
val startDate: Long?,
val endDate: Long?
) : State() {
data class RotationRange(
val distance: Double,
val startDate: Long,
val endDate: Long,
val maxSpeed: Double,
val avgSpeed: Double,
val rotations: Float,
)
}
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect(){
object NavigateUp : Navigation()
data class NavigateToDelete(
val serial: String
) : Navigation()
}
}
}

View File

@ -0,0 +1,496 @@
package llc.arma.ble.app.ui.screen.rotation.statistic
import android.icu.text.SimpleDateFormat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowForward
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material3.Card
import androidx.compose.material3.DatePicker
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
import kotlin.math.pow
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RotationStatisticScreen(
onNavigation: (RotationStatisticContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<RotationStatisticViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.onEach {
when(it){
is RotationStatisticContract.Effect.Navigation -> onNavigation(it)
}
}.launchIn(this)
}
Column(
modifier = Modifier.fillMaxSize()
) {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(RotationStatisticContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
modifier = Modifier.weight(1f),
text = "Обороты",
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(RotationStatisticContract.Event.OnNavigateToDelete)
}
) {
Icon(
imageVector = Icons.Rounded.Delete,
contentDescription = null
)
}
}
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(12.dp)
) {
if(state is RotationStatisticContract.State.Display){
var showStartDateSelector by remember {
mutableStateOf(false)
}
var showEndDateSelector by remember {
mutableStateOf(false)
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Box(
modifier = Modifier
.weight(1f)
.width(IntrinsicSize.Max)
.height(IntrinsicSize.Max)
) {
TextField(
readOnly = true,
value = dateFormat.format(Date(state.startDate ?: state.ranges.minOfOrNull { it.startDate } ?: 0)),
onValueChange = {},
label = {
Text(text = "Начало")
},
)
Box(
modifier = Modifier
.fillMaxSize()
.clickable { showStartDateSelector = true }
) {}
}
Box(
modifier = Modifier
.weight(1f)
.width(IntrinsicSize.Max)
.height(IntrinsicSize.Max)
) {
TextField(
readOnly = true,
value = dateFormat.format(Date(state.endDate ?: state.ranges.maxOfOrNull { it.endDate } ?: 0)),
onValueChange = {},
label = {
Text(text = "Завершение")
}
)
Box(
modifier = Modifier
.fillMaxSize()
.clickable { showEndDateSelector = true }
) {}
}
}
TextField(
singleLine = true,
value = state.radius?.toString() ?: "",
onValueChange = {
viewModel.setEvent(
RotationStatisticContract.Event.OnRadiusChanged(
it.filter { it.isDigit() }.toLongOrNull()
)
)
},
label = {
Text(text = "Радиус колеса (мм.)")
},
modifier = Modifier.fillMaxWidth()
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
Text(text = "Пройденное расстояние")
Text(text = "Движение вперед")
Text(text = "Движение назад")
Text(text = "Стоянка")
}
Column {
val duration = state.ranges.filter { it.distance == 0.0 }.sumOf { it.endDate - it.startDate }
val hours = duration / TimeUnit.HOURS.toMillis(1)
val minutes = (duration - (hours * TimeUnit.HOURS.toMillis(1))) / TimeUnit.MINUTES.toMillis(1)
val seconds = (duration - (hours * TimeUnit.HOURS.toMillis(1)) - (minutes * TimeUnit.MINUTES.toMillis(1))) / TimeUnit.SECONDS.toMillis(1)
Text(text = state.ranges.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
Text(text = state.ranges.filter { it.distance > 0 }.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
Text(text = state.ranges.filter { it.distance < 0 }.sumOf { it.distance.absoluteValue }.roundTo().toString() + " м.")
Text(text = "${hours}ч ${minutes}м. ${seconds}с.")
}
}
var reversed by remember {
mutableStateOf(true)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Порядок")
IconButton(
onClick = { reversed = reversed.not() }
) {
Icon(
imageVector = if (reversed) Icons.Rounded.ArrowBack else Icons.Rounded.ArrowForward,
contentDescription = null
)
}
}
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
items(items = state.ranges.let { if (reversed) it.reversed() else it}){
DataRange(range = it)
}
}
if(showStartDateSelector) {
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = {
showEndDateSelector = false
}
) {
Surface(
shape = RoundedCornerShape(16.dp)
) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = state.startDate
)
Column{
DatePicker(state = datePickerState)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
modifier = Modifier
.align(Alignment.End)
.padding(12.dp)
) {
TextButton(
onClick = {
showStartDateSelector = false
}
) {
Text(text = "Отмена")
}
Button(
onClick = {
viewModel.setEvent(
RotationStatisticContract.Event.OnStartDateChanged(
datePickerState.selectedDateMillis ?: state.startDate
)
)
showStartDateSelector = false
}
) {
Text(text = "ок")
}
}
}
}
}
}
if(showEndDateSelector) {
Dialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
onDismissRequest = {
showEndDateSelector = false
}
) {
Surface(
shape = RoundedCornerShape(16.dp)
) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = state.endDate
)
Column {
DatePicker(state = datePickerState)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
modifier = Modifier
.align(Alignment.End)
.padding(12.dp)
) {
TextButton(
onClick = {
showEndDateSelector = false
}
) {
Text(text = "Отмена")
}
Button(
onClick = {
viewModel.setEvent(
RotationStatisticContract.Event.OnStopDateChanged(
datePickerState.selectedDateMillis ?: state.endDate
)
)
showEndDateSelector = false
}
) {
Text(text = "ок")
}
}
}
}
}
}
}
}
}
}
fun Double.roundTo(numFractionDigits: Int = 2): Double {
val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor
}
private fun Double.round(decimals: Int = 2): Double = "%.${decimals}f".format(this).toDouble()
private val dateTimeFormat = SimpleDateFormat("dd.MM HH:mm:ss")
private val dateFormat = SimpleDateFormat("dd.MM HH:mm:ss")
@Composable
private fun DataRange(range: RotationStatisticContract.State.Display.RotationRange){
Card {
Column(
modifier = Modifier
.padding(12.dp)
.width(IntrinsicSize.Max)
) {
val duration = range.endDate - range.startDate
val hours = duration / TimeUnit.HOURS.toMillis(1)
val minutes = (duration - (hours * TimeUnit.HOURS.toMillis(1))) / TimeUnit.MINUTES.toMillis(1)
val seconds = (duration - (hours * TimeUnit.HOURS.toMillis(1)) - (minutes * TimeUnit.MINUTES.toMillis(1))) / TimeUnit.SECONDS.toMillis(1)
val directionText = if(range.distance > 0){
"-->"
}else{
if(range.distance < 0) {
"<--"
} else {
"<<P>>"
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
Text(text = "Начало")
Text(text = "Конец")
}
Column {
Text(text = dateTimeFormat.format(Date(range.startDate)))
Text(text = dateTimeFormat.format(Date(range.endDate)))
}
}
Text(text = "Направление: " + directionText)
HorizontalDivider()
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
Text(text = "Время")
if(range.distance != 0.0) {
Text(text = "Расстояние")
Text(text = "Vmax.")
Text(text = "Vср.")
Text(text = "Обороты")
}
}
Column {
Text(text = "${hours}ч ${minutes}м. ${seconds}с.")
if(range.distance != 0.0) {
Text(text = range.distance.absoluteValue.roundTo().toString() + " м.")
Text(text = range.maxSpeed.roundTo().toString() + " Км/ч")
Text(text = range.avgSpeed.roundTo().toString() + " Км/ч")
Text(text = range.rotations.absoluteValue.toDouble().roundTo().toString())
}
}
}
}
}
}

View File

@ -0,0 +1,256 @@
package llc.arma.ble.app.ui.screen.rotation.statistic
import android.icu.util.GregorianCalendar
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.core.extension.sumOf
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.GetRotationsBySerial
import llc.arma.ble.domain.usecase.GetWheelRadiusBySerial
import llc.arma.ble.domain.usecase.SetWheelRadius
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.absoluteValue
fun <T> Collection<T>.splitBy(separator: (T) -> Any?): List<List<T>> {
val result = mutableListOf<MutableList<T>>()
var newSublist = true
for (item in this) {
newSublist = (separator(item) != result.lastOrNull()?.lastOrNull()?.let { separator(it) })
if (newSublist || result.lastOrNull() == null)
result += mutableListOf<T>()
result.last() += item
}
return result
}
@HiltViewModel
class RotationStatisticViewModel @Inject constructor(
private val setWheelRadius: SetWheelRadius,
private val getWheelRadius: GetWheelRadiusBySerial,
private val getRotationsBySerial: GetRotationsBySerial,
private val savedStateHandle: SavedStateHandle
) : BaseViewModel<RotationStatisticContract.State, RotationStatisticContract.Event, RotationStatisticContract.Effect>() {
init {
viewModelScope.launch {
load()
}
}
override fun setInitialState() = RotationStatisticContract.State.Loading
override fun handleEvents(event: RotationStatisticContract.Event) {
when(event){
is RotationStatisticContract.Event.OnRadiusChanged -> reduce(viewState.value, event)
is RotationStatisticContract.Event.OnStartDateChanged -> reduce(viewState.value, event)
is RotationStatisticContract.Event.OnStopDateChanged -> reduce(viewState.value, event)
is RotationStatisticContract.Event.OnNavigateToDelete -> reduce(viewState.value, event)
is RotationStatisticContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private suspend fun load(){
savedStateHandle.get<String>("serial")?.let {
val state = viewState.value
val r = getWheelRadius(it)?.toFloat() ?: 500f
val rCm = r / 10f
val circumference = 2 * Math.PI * rCm
Log.d("circumference", circumference.toString())
var rotations = getRotationsBySerial(it)
if(state is RotationStatisticContract.State.Display){
rotations = rotations.filter {
it.date >= (state.startDate ?: 0) && it.date <= (state.endDate ?: Long.MAX_VALUE)
}
}
val split: List<RotationStatisticContract.State.Display.RotationRange> = if(rotations.size < 2) {
emptyList()
}else {
val pointDuration = rotations[1].date - rotations[0].date
rotations.splitBy {
Log.d("rDate", it.date.toString())
if (it.rotations > 0) {
true
} else {
if (it.rotations < 0) {
false
} else {
null
}
}
}.map { range ->
val start = range.minOf { it.date } - pointDuration
val end = range.maxOf { it.date }
val speeds = range.map {
val km = ((it.rotations.absoluteValue / 8f) * circumference) / 100_000f
km / (pointDuration.toFloat() / TimeUnit.HOURS.toMillis(1).toFloat())
}
RotationStatisticContract.State.Display.RotationRange(
distance = ((range.sumOf { it.rotations.toFloat() } / 8f) * circumference) / 100f,
startDate = start,
endDate = end,
maxSpeed = speeds.max(),
avgSpeed = speeds.average(),
rotations = range.sumOf { it.rotations.toFloat() } / 8f
)
}
}
setState {
when(this){
is RotationStatisticContract.State.Display -> {
copy(
rotations = rotations,
ranges = split
)
}
RotationStatisticContract.State.Loading -> {
RotationStatisticContract.State.Display(
rotations = rotations,
ranges = split,
radius = r.toLong(),
startDate = null,
endDate = null
)
}
}
}
} ?: throw IllegalArgumentException()
}
private fun reduce(
state: RotationStatisticContract.State,
event: RotationStatisticContract.Event.OnRadiusChanged
) {
if(state is RotationStatisticContract.State.Display){
setState {
state.copy(radius = event.radius)
}
viewModelScope.launch {
setWheelRadius(savedStateHandle.get<String>("serial")!!, event.radius ?: 100L)
load()
}
}
}
private fun reduce(
state: RotationStatisticContract.State,
event: RotationStatisticContract.Event.OnStartDateChanged
) {
if(state is RotationStatisticContract.State.Display){
val mCalendar = GregorianCalendar()
val mTimeZone = mCalendar.timeZone
val mGMTOffset: Int = mTimeZone.rawOffset
val start = event.start?.let {
TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(event.start)) - mGMTOffset
}
Log.d("date", start.toString() + " " + event.start)
setState {
state.copy(startDate = start)
}
viewModelScope.launch {
load()
}
}
}
private fun reduce(
state: RotationStatisticContract.State,
event: RotationStatisticContract.Event.OnStopDateChanged
) {
if(state is RotationStatisticContract.State.Display){
val mCalendar = GregorianCalendar()
val mTimeZone = mCalendar.timeZone
val mGMTOffset: Int = mTimeZone.rawOffset
val stop = event.stop?.let {
TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(event.stop) + 1) - mGMTOffset - 1_000
}
setState {
state.copy(endDate = stop)
}
viewModelScope.launch {
load()
}
}
}
private fun reduce(
state: RotationStatisticContract.State,
event: RotationStatisticContract.Event.OnNavigateUp
) {
setEffect {
RotationStatisticContract.Effect.Navigation.NavigateUp
}
}
private fun reduce(
state: RotationStatisticContract.State,
event: RotationStatisticContract.Event.OnNavigateToDelete
) {
setEffect {
RotationStatisticContract.Effect.Navigation.NavigateToDelete(savedStateHandle.get<String>("serial")!!)
}
}
}

View File

@ -0,0 +1,16 @@
package llc.arma.ble.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
import llc.arma.ble.data.model.RotationEntity
import llc.arma.ble.data.model.WheelEntity
@Database(
entities = [RotationEntity::class, WheelEntity::class],
version = 1
)
abstract class AppDatabase : RoomDatabase() {
abstract fun getRotationsDao(): RotationsDao
}

View File

@ -0,0 +1,31 @@
package llc.arma.ble.data.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import llc.arma.ble.data.model.RotationEntity
import llc.arma.ble.data.model.WheelEntity
@Dao
interface RotationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(rotations: WheelEntity)
@Query("select * from wheel where bleId = :serial")
suspend fun getWheelBySerial(serial: String): WheelEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(rotations: List<RotationEntity>)
@Query("delete from rotation where bleId = :serial")
suspend fun deleteBySerial(serial: String)
@Query("delete from rotation where bleId = :serial and date > :date")
suspend fun deleteBySerialAndDateMoreThan(serial: String, date: Long)
@Query("select * from rotation where bleId = :serial order by date asc")
suspend fun getBySerialSortedByDate(serial: String): List<RotationEntity>
}

View File

@ -0,0 +1,19 @@
package llc.arma.ble.data.model
import androidx.room.Entity
import androidx.room.Index
@Entity(
tableName = "rotation",
indices = [Index(unique = true, value = ["bleId", "date"])],
primaryKeys = ["bleId", "date"]
)
class RotationEntity(
val bleId: String,
val date: Long,
val rotations: Long
)

View File

@ -0,0 +1,18 @@
package llc.arma.ble.data.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "wheel",
indices = [Index(unique = true, value = ["bleId"])]
)
class WheelEntity(
@PrimaryKey
val bleId: String,
val radius: Long
)

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.os.SystemClock import android.os.SystemClock
@ -14,12 +14,13 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.data.extensions.checkPermission import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.extensions.fromByte import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.fromByte
import llc.arma.ble.data.extensions.info import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.extensions.sendData import llc.arma.ble.data.repository.extensions.info
import llc.arma.ble.data.extensions.toTemperature import llc.arma.ble.data.repository.extensions.sendData
import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
@ -67,7 +68,8 @@ val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002
@Singleton @Singleton
class BleRepositoryImpl @Inject constructor( class BleRepositoryImpl @Inject constructor(
private val app: Application private val app: Application,
private val rotationsDao: RotationsDao
) : BleRepository { ) : BleRepository {
val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>()) val resultList: MutableMap<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())
@ -536,13 +538,17 @@ class BleRepositoryImpl @Inject constructor(
request.tx?.let { request.tx?.let {
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(txWriteUUID)?.write( connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
txWriteUUID
)?.write(
DataByteArray.from(it.sendData) DataByteArray.from(it.sendData)
) ?: return Result.failure(BleException.UnexpectedResponse) ) ?: return Result.failure(BleException.UnexpectedResponse)
} }
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(flashWriteUUID)!!.write( connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
flashWriteUUID
)!!.write(
DataByteArray.from(9) DataByteArray.from(9)
) )
@ -579,6 +585,8 @@ class BleRepositoryImpl @Inject constructor(
(this shr (it * Byte.SIZE_BITS)).toByte() (this shr (it * Byte.SIZE_BITS)).toByte()
}.toByteArray() }.toByteArray()
rotationsDao.deleteBySerial(serial)
return if(app.checkPermission()) { return if(app.checkPermission()) {
val connection = val connection =

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
@ -6,11 +6,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.extensions.fromByte import llc.arma.ble.data.repository.extensions.fromByte
import llc.arma.ble.data.extensions.get2byteShortAt import llc.arma.ble.data.repository.extensions.get2byteShortAt
import llc.arma.ble.data.extensions.get2byteUIntAt import llc.arma.ble.data.repository.extensions.get2byteUIntAt
import llc.arma.ble.data.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState

View File

@ -1,14 +1,13 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.extensions.get2byteShortAt import llc.arma.ble.data.repository.extensions.get2byteShortAt
import llc.arma.ble.data.extensions.sendData import llc.arma.ble.data.repository.extensions.sendData
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble

View File

@ -1,21 +1,16 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattDescriptor
import android.os.Build
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.extensions.get2byteShortAt import llc.arma.ble.data.repository.extensions.get2byteShortAt
import llc.arma.ble.data.extensions.get2byteUIntAt import llc.arma.ble.data.repository.extensions.get2byteUIntAt
import llc.arma.ble.data.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.extensions.sendData import llc.arma.ble.data.repository.extensions.sendData
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
@ -27,8 +22,6 @@ import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.util.UUID import java.util.UUID

View File

@ -1,19 +1,14 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.os.Build
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.extensions.get2byteUIntAt import llc.arma.ble.data.repository.extensions.get2byteUIntAt
import llc.arma.ble.data.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.extensions.toTemperature import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState

View File

@ -0,0 +1,49 @@
package llc.arma.ble.data.repository
import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.model.RotationEntity
import llc.arma.ble.data.model.WheelEntity
import llc.arma.ble.domain.model.Rotation
import llc.arma.ble.domain.repository.RotationsRepository
import javax.inject.Inject
class RotationsRepositoryImpl @Inject constructor(
private val dao: RotationsDao
) : RotationsRepository {
override suspend fun saveRotations(rotations: List<Rotation>) {
dao.deleteBySerialAndDateMoreThan(rotations.first().bleId, rotations.minOf { it.date } - 8)
dao.save(
rotations.map {
RotationEntity(
bleId = it.bleId,
rotations = it.rotations,
date = it.date
)
}
)
}
override suspend fun getRotationsBySerial(serial: String): List<Rotation> {
return dao.getBySerialSortedByDate(serial).map {
Rotation(
bleId = it.bleId,
rotations = it.rotations,
date = it.date
)
}
}
override suspend fun deleteRotationsBySerial(serial: String) {
dao.deleteBySerial(serial)
}
override suspend fun getWheelRadiusBySerial(serial: String): Long? {
return dao.getWheelBySerial(serial)?.radius
}
override suspend fun setWheelRadius(serial: String, radius: Long) {
dao.save(WheelEntity(serial, radius))
}
}

View File

@ -1,9 +1,8 @@
package llc.arma.ble.data package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.icu.text.SimpleDateFormat import android.icu.text.SimpleDateFormat
import android.os.Environment import android.os.Environment
import android.util.Log
import llc.arma.ble.R import llc.arma.ble.R
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.XlsxRepository import llc.arma.ble.domain.repository.XlsxRepository

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data.extensions package llc.arma.ble.data.repository.extensions
import android.Manifest import android.Manifest
import android.app.Application import android.app.Application

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data.extensions package llc.arma.ble.data.repository.extensions
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data.extensions package llc.arma.ble.data.repository.extensions
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult

View File

@ -1,4 +1,4 @@
package llc.arma.ble.data.extensions package llc.arma.ble.data.repository.extensions
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder

View File

@ -0,0 +1,14 @@
package llc.arma.ble.domain.model
import androidx.room.Entity
import androidx.room.Index
data class Rotation(
val bleId: String,
val date: Long,
val rotations: Long
)

View File

@ -0,0 +1,17 @@
package llc.arma.ble.domain.repository
import llc.arma.ble.domain.model.Rotation
interface RotationsRepository {
suspend fun saveRotations(rotations: List<Rotation>)
suspend fun getRotationsBySerial(serial: String): List<Rotation>
suspend fun deleteRotationsBySerial(serial: String)
suspend fun getWheelRadiusBySerial(serial: String): Long?
suspend fun setWheelRadius(serial: String, radius: Long)
}

View File

@ -0,0 +1,17 @@
package llc.arma.ble.domain.usecase
import llc.arma.ble.domain.model.Rotation
import llc.arma.ble.domain.repository.RotationsRepository
import javax.inject.Inject
class DeleteRotationsBySerial @Inject constructor (
private val rotationsRepository: RotationsRepository
) {
suspend operator fun invoke(serial: String) {
rotationsRepository.deleteRotationsBySerial(serial)
}
}

View File

@ -1,20 +1,49 @@
package llc.arma.ble.domain.usecase package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.Rotation
import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.repository.RotationsRepository
import java.time.LocalDateTime
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class GetAccelerometerHistoryBySerial @Inject constructor( class GetAccelerometerHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository,
private val rotationsRepository: RotationsRepository
) { ) {
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> { suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
return bleRepository.getAccelerometerHistoryBySerial(serial) return bleRepository.getAccelerometerHistoryBySerial(serial).onEach {
it.onSuccess { progress ->
if(progress is ProgressState.Finished){
val rotations = progress.data.mapNotNull { point ->
if(point is Ble.Accelerometer.HistoryPoint.Rotation){
Rotation(
bleId = serial,
date = TimeUnit.SECONDS.toMillis(TimeUnit.MILLISECONDS.toSeconds(point.date)),
rotations = point.value
)
}else{
null
}
}
if(rotations.isNotEmpty()){
rotationsRepository.saveRotations(rotations)
}
}
}
}
} }

View File

@ -0,0 +1,17 @@
package llc.arma.ble.domain.usecase
import llc.arma.ble.domain.model.Rotation
import llc.arma.ble.domain.repository.RotationsRepository
import javax.inject.Inject
class GetRotationsBySerial @Inject constructor (
private val rotationsRepository: RotationsRepository
) {
suspend operator fun invoke(serial: String): List<Rotation> {
return rotationsRepository.getRotationsBySerial(serial)
}
}

View File

@ -0,0 +1,14 @@
package llc.arma.ble.domain.usecase
import llc.arma.ble.domain.repository.RotationsRepository
import javax.inject.Inject
class GetWheelRadiusBySerial @Inject constructor(
private val rotationsRepository: RotationsRepository
) {
suspend operator fun invoke(serial: String): Long? {
return rotationsRepository.getWheelRadiusBySerial(serial)
}
}

View File

@ -0,0 +1,14 @@
package llc.arma.ble.domain.usecase
import llc.arma.ble.domain.repository.RotationsRepository
import javax.inject.Inject
class SetWheelRadius @Inject constructor(
private val rotationsRepository: RotationsRepository
) {
suspend operator fun invoke(serial: String, radius: Long){
rotationsRepository.setWheelRadius(serial, radius)
}
}