diff --git a/.idea/misc.xml b/.idea/misc.xml index 773fe0f..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 9312e0e..3cfef6a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,10 +11,10 @@ android { defaultConfig { applicationId "llc.arma.ble" - minSdk 24 + minSdk 26 targetSdk 33 - versionCode 1 - versionName "1.0" + versionCode 12 + versionName "1.2.12" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -46,6 +46,13 @@ android { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "Arma BLE v${defaultConfig.versionName}.apk" + } + } + } dependencies { @@ -76,8 +83,10 @@ dependencies { implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta" - implementation "com.patrykandpatrick.vico:core:1.6.6" - implementation "com.patrykandpatrick.vico:compose:1.6.6" - implementation "com.patrykandpatrick.vico:compose-m3:1.6.6" + implementation "com.patrykandpatrick.vico:core:1.7.1" + implementation "com.patrykandpatrick.vico:compose:1.7.1" + implementation "com.patrykandpatrick.vico:compose-m3:1.7.1" + + implementation files('libs/poishadow-all.jar') } \ No newline at end of file diff --git a/app/libs/poishadow-all.jar b/app/libs/poishadow-all.jar new file mode 100644 index 0000000..177eba8 Binary files /dev/null and b/app/libs/poishadow-all.jar differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d50d78..f9ed2e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,13 @@ + + + + + + android:name=".app.framework.App" + android:requestLegacyExternalStorage="true"> + + + + + + Unit>({}) } + if(modalState.currentValue == ModalBottomSheetValue.Hidden){ + sheetContent = {} + } + CompositionLocalProvider( LocalBottomDialogState provides BottomState( sheetState = modalState, @@ -125,20 +135,37 @@ class MainActivity : ComponentActivity() { ) { val multiplePermissionsState = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberMultiplePermissionsState( listOf( + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) ) - } else { - rememberMultiplePermissionsState( - listOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION + + }else{ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + rememberMultiplePermissionsState( + listOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT + ) ) - ) + } else { + rememberMultiplePermissionsState( + listOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + ) + } } if (multiplePermissionsState.allPermissionsGranted) { diff --git a/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt b/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt index a210baf..6b99a7f 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/common/BottomDialog.kt @@ -38,7 +38,7 @@ class BottomDialogState @OptIn(ExperimentalMaterialApi::class) constructor( content: @Composable () -> Unit ){ setContent(content) - if(sheetState?.currentValue != ModalBottomSheetValue.Expanded) + //if(sheetState?.currentValue != ModalBottomSheetValue.Expanded) sheetState?.show() } diff --git a/app/src/main/java/llc/arma/ble/app/ui/common/SignalLevel.kt b/app/src/main/java/llc/arma/ble/app/ui/common/SignalLevel.kt new file mode 100644 index 0000000..3bcede3 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/common/SignalLevel.kt @@ -0,0 +1,50 @@ +package llc.arma.ble.app.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun SignalLevel( + modifier: Modifier = Modifier, + maxLevel: Int = 5, + level: Int +){ + + val step = (16 - 4) / 4 + + Row( + modifier = modifier.height(16.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.Bottom + ) { + + for(col in 0..4 step 1){ + Surface( + color = LocalContentColor.current.copy( + alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled + ), + shape = CircleShape, + modifier = Modifier + .width(4.dp) + .defaultMinSize(minHeight = 4.dp) + .height(((col + 1) * step).dp) + ) { } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt index c0296c5..aaed0c2 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt @@ -35,6 +35,13 @@ class BleMapper @Inject constructor( is Ble.Accelerometer -> { BleView.Accelerometer( info = input.info, + state = BleView.BleState( + tx = txMapper.map(input.state.tx) + ), + accelerometerState = BleView.Accelerometer.AccelerometerState( + saveHistory = input.accelerometerState.saveHistory, + historyInterval = input.accelerometerState.historyInterval + ) ) } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt index c8717ad..012e920 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt @@ -34,7 +34,14 @@ class BleViewMapper @Inject constructor( is BleView.Accelerometer -> { Ble.Accelerometer( - info = input.info + info = input.info, + state = Ble.BleState( + tx = txMapper.map(input.state.tx) + ), + accelerometerState = Ble.Accelerometer.AccelerometerState( + saveHistory = input.accelerometerState.saveHistory, + historyInterval = input.accelerometerState.historyInterval, + ) ) } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt index eb72931..7f134cb 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt @@ -3,15 +3,31 @@ package llc.arma.ble.app.ui.model import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.BleInfo +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode sealed class BleView( val info: BleInfo ) { class Accelerometer( - info: BleInfo - ) : BleView(info) + info: BleInfo, + val state: BleState, + val accelerometerState: AccelerometerState + ) : BleView(info) { + + class AccelerometerState( + saveHistory: Ble.Accelerometer.History, + historyInterval: Long, + ) { + var saveHistory by mutableStateOf(saveHistory) + var historyInterval by mutableStateOf(historyInterval) + + } + + } class Beacon( info: BleInfo, @@ -58,9 +74,27 @@ sealed class BleView( MINUS_4(-4), ZERO(0), PLUS_3(3), - PLUS_4(4) + PLUS_4(4); + + val powerPercentage: Int + get() { + return when(this){ + MINUS_40 -> 1 + MINUS_20 -> 5 + MINUS_16 -> 7 + MINUS_12 -> 10 + MINUS_8 -> 16 + MINUS_4 -> 20 + ZERO -> 40 + PLUS_3 -> 80 + PLUS_4 -> 100 + } + } + } + + } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt index d86c23c..8e1f72c 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt @@ -48,7 +48,7 @@ class BleListContract { data class Filter( val name: String = "", val mac: String = "", - val rssi: ClosedFloatingPointRange = (-100f)..(-30f), + val rssi: ClosedFloatingPointRange = (-100f)..(-10f), val bleType: BleInfo.Type? = null ) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt index 40c3dfb..efa91a7 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -29,9 +30,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import llc.arma.ble.app.ui.common.SignalLevel import llc.arma.ble.app.ui.common.rememberBottomDialogState import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.ConnectedBleInfo +import kotlin.math.pow @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -119,7 +122,14 @@ fun BleListScreen( } ) - if(state.bleList.isEmpty()){ + val filteredData = state.bleList.filter { + (it.type == state.filter.bleType || state.filter.bleType == null) && + it.name.contains(state.filter.name) && + it.serial.contains(state.filter.mac) && + state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) + } + + if(filteredData.isEmpty()){ LinearProgressIndicator( strokeCap = StrokeCap.Round, modifier = Modifier @@ -128,37 +138,44 @@ fun BleListScreen( ) } - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxSize() - ) { + if(filteredData.isEmpty()){ - items(items = state.connectedBleList){ + Box(modifier = Modifier.fillMaxSize()){ + Text( + modifier = Modifier.align(Alignment.Center), + style = MaterialTheme.typography.titleMedium, + text = "Метки в области не найдены" + ) + } + + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxSize() + ) { + + items(items = state.connectedBleList){ + + ConnectedBleItem(ble = it) { + viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial)) + } + + } + + + + items(items = filteredData.sortedBy { it.name }.reversed()) { + + BleItem( + ble = it, + onClick = { + viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial)) + } + ) - ConnectedBleItem(ble = it) { - viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial)) } } - - val filteredData = state.bleList.filter { - (it.type == state.filter.bleType || state.filter.bleType == null) && - it.name.contains(state.filter.name) && - it.serial.contains(state.filter.mac) && - state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) - } - - items(items = filteredData.sortedBy { it.name }.reversed()) { - - BleItem( - ble = it, - onClick = { - viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial)) - } - ) - - } - } } @@ -184,6 +201,16 @@ private fun ItemIcon( } +private fun Int.toSignalLevel(): Int { + return when(this){ + in -30 downTo -52 -> 4 + in -51 downTo -63 -> 3 + in -62 downTo -75 -> 2 + in -74 downTo -89 -> 1 + else -> 0 + } +} + @Composable private fun BleItem( ble: BleInfo, @@ -199,28 +226,27 @@ private fun BleItem( val highAlpha = ContentAlpha.high val disabledAlpha = ContentAlpha.disabled - var alpha by remember { - mutableStateOf( - if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){ - disabledAlpha - } else { - highAlpha - } + + var time by remember { + mutableLongStateOf( + SystemClock.elapsedRealtime() ) } LaunchedEffect(ble.scanTime) { while(true) { - alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){ - disabledAlpha - } else { - highAlpha - } - - delay(800) + time = SystemClock.elapsedRealtime() + delay(100) } } + var alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){ + disabledAlpha + } else { + highAlpha + } + + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -234,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 { @@ -255,6 +305,26 @@ private fun BleItem( text = ble.serial ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.alpha(0.7f) + ) { + + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Rounded.CompareArrows, + contentDescription = null + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м." + ) + + } + Row( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -264,12 +334,10 @@ private fun BleItem( modifier = Modifier.alpha(0.7f) ) { - Icon( - modifier = Modifier.size(16.dp), - imageVector = Icons.Rounded.NetworkCell, - contentDescription = null - ) + SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0) + Spacer(modifier = Modifier.width(4.dp)) + Box { Text( @@ -329,17 +397,10 @@ private fun BleItem( modifier = Modifier.alpha(0.7f) ) { - val color = if(ble.batteryLevel < 100){ - MaterialTheme.colorScheme.error - } else { - LocalContentColor.current - } - Icon( modifier = Modifier.size(16.dp), imageVector = Icons.Rounded.ArrowRightAlt, - contentDescription = null, - tint = color + contentDescription = null ) Box { @@ -356,7 +417,7 @@ private fun BleItem( modifier = Modifier.alpha(0f) ) - val lastAdv = ((SystemClock.elapsedRealtime() - ble.scanTime) / 1_000) + val lastAdv = ((time - ble.scanTime) / 1_000) Text( style = MaterialTheme.typography.bodyMedium, diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt index 7ed33d2..bc44e7c 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt @@ -7,14 +7,17 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.BaseViewModel +import llc.arma.ble.domain.usecase.ExportToXlsx import llc.arma.ble.domain.usecase.GetBleAroundFlow import llc.arma.ble.domain.usecase.GetConnectedBleDevices +import llc.arma.ble.domain.usecase.MeasureData import javax.inject.Inject @HiltViewModel class BleListViewModel @Inject constructor( getBleAroundFlow: GetBleAroundFlow, - getConnectedBleDevices: GetConnectedBleDevices + getConnectedBleDevices: GetConnectedBleDevices, + exportToXlsx: ExportToXlsx ) : BaseViewModel() { init { diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt index 9ab47dd..041ed61 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt @@ -244,8 +244,8 @@ fun Filter( onValueChange = { onEvent(BleListContract.Event.OnRssiRangeChanged(it)) }, - valueRange = (-100f)..(-30f), - steps = 69, + valueRange = (-100f)..(-10f), + steps = 89, colors = SliderDefaults.colors( activeTickColor = MaterialTheme.colorScheme.primary, inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt index 314ffa9..0d066ba 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt @@ -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 { diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt index 9b3fa1b..e17bf59 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt @@ -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) + ) + } } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt index f972246..00f76b3 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt @@ -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("serial")!!) + } + } + } + } + private fun reduce( state: ConnectionContract.State, event: ConnectionContract.Event.OnNavigateUp diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerContract.kt index f2df050..9d76190 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerContract.kt @@ -4,21 +4,102 @@ 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.thermometer.ThermometerContract 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 class AccelerometerContract { sealed class Event : ViewEvent { - object OnShowAccelerometerMeasure : Event() + object OnShowAccelerometerAccel : Event() + object OnHideAccelerometerAccel : Event() - object OnHideAccelerometerMeasure : Event() + object OnShowAccelerometerSpectre : Event() + object OnHideAccelerometerSpectre : Event() + + object OnShowAccelerometerHistory : Event() + object OnHideAccelerometerHistory : Event() + + data class OnAccelViewModeEdit( + val next: Next + ) : Event() + + enum class Next { + ACCEL, SPECTRE, HISTORY + } + + data class OnAccelScaleEdit( + val next: Next + ) : Event() + + object OnAccelEdit : Event() + object OnSpectreEdit : Event() + object OnHistoryEdit : Event() + object OnFftFrequencyEdit : Event() + + object OnFftAxisEdit : Event() + object OnFftModeEdit : Event() + + object OnPowerEdit : Event() + + object OnShowWriteBlePreview : Event() + + object OnWriteBle : Event() + + object OnHideWriteBlePreview : Event() + + object OnChangePassword : Event() + object OnSaveIntervalEdit : Event() + object OnHideHistoryEdit : Event() data class OnBleChanged( val ble: Ble.Accelerometer, ): Event() + data class OnPowerChanged( + val tx: BleView.BleState.TX + ) : Event() + + data class OnAccelViewModelChanged( + val mode: AccelViewMode + ) : Event() + + data class OnHistoryViewModeChanged( + val mode: AccelViewMode + ) : Event() + + data class OnFftFrequencyChanged( + val frequency: FftFrequency + ) : Event() + + data class OnFftAxisChanged( + val axis: FftAxis + ) : Event() + + data class OnFftModeChanged( + val mode: FftViewMode + ) : Event() + + data class OnAccelScaleChanged( + val scale: AccelScale + ) : Event() + + data class OnHistoryScaleChanged( + val scale: AccelScale + ) : Event() + + data class OnSaveHistoryChanged( + val save: Boolean + ) : Event() + + data class OnSaveIntervalChanged( + val interval: Long + ) : Event() + } sealed class State : ViewState { @@ -28,13 +109,73 @@ class AccelerometerContract { data class Display( val origin: Ble.Accelerometer, val accelerometer: BleView.Accelerometer, - ) : State() + val writeState: WriteState?, + val accelViewMode: AccelViewMode, + val accelScale: AccelScale, + val fftViewMode: FftViewMode, + val fftAxis: FftAxis, + val fftFrequency: FftFrequency, + ) : State() { + + sealed class WriteState { + + data class DisplayPreview( + val writeRequest: Ble.Accelerometer.WriteRequest + ) : WriteState() + + data class Writing( + val writeRequest: Ble.Accelerometer.WriteRequest + ) : WriteState() + + object Success : WriteState() + + object Failure : WriteState() + + } + + } } sealed class Effect : ViewSideEffect { - object ShowAccelerometerMeasure : Effect() + object ShowAccelerometerAccel : Effect() + object ShowAccelerometerSpectre : Effect() + object ShowAccelerometerHistory : Effect() + + object ShowPowerPicker : Effect() + object HidePowerPicker : Effect() + + object ShowWriteBle : Effect() + object HideWriteBle : Effect() + + object HideHistoryEdit : Effect() + + data class ShowAccelViewEdit( + val next: Event.Next + ) : Effect() + + data class ShowAccelScaleEdit( + val next: Event.Next + ) : Effect() + + object ShowAccelEdit : Effect() + object ShowSpectreEdit : Effect() + object ShowFftFrequencyEdit : Effect() + object ShowFftAxisEdit : Effect() + object ShowFftModeEdit : Effect() + + + object HideIntervalPicker : Effect() + object ShowIntervalPicker : Effect() + + object ShowHistoryEdit : Effect() + + sealed class Navigation : Effect() { + + object NavigateToChangePassword : Navigation() + + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerScreen.kt index 8b95fc6..deb3da6 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerScreen.kt @@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ModalBottomSheetValue import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.hilt.navigation.compose.hiltViewModel @@ -15,19 +17,33 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.rememberBottomDialogState -import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerMeasure +import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelEdit +import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftAxisEdit +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.AccelScaleEdit +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.DisplayState +import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.HistoryEdit +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_HISTORY + HISTORY, ACCEL_SCALE, SPECTRE_SCALE, HISTORY_MODE_EDIT, HISTORY_SCALE, HISTORY_EDIT, ACCEL_EDIT, 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() @@ -43,30 +59,402 @@ fun AccelerometerScreen( mutableStateOf(null) } + LaunchedEffect( + key1 = bottomDialog.sheetState?.currentValue, + block = { + if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) { + bottomDialog.setContent({}) + sheetPage = null + } + } + ) + + val scope = rememberCoroutineScope() + LaunchedEffect(sheetPage) { when (sheetPage) { - SheetPage.MEASURE_HISTORY -> launch { + SheetPage.HISTORY -> launch { val currentState = viewModel.viewState.value if (currentState is AccelerometerContract.State.Display) { bottomDialog.show { - AccelerometerMeasure(ble = currentState.accelerometer.info) + AccelerometerHistory( + ble = currentState.accelerometer.info, + accelMode = currentState.accelViewMode, + fftAxis = currentState.fftAxis, + fftMode = currentState.fftViewMode, + frequency = currentState.fftFrequency, + accelScale = currentState.accelScale + ) } } } + SheetPage.ACCEL -> launch { + val currentState = viewModel.viewState.value + + if (currentState is AccelerometerContract.State.Display) { + bottomDialog.show { + 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, + fftMode = currentState.fftViewMode, + frequency = currentState.fftFrequency, + accelScale = currentState.accelScale + ) + } + } + } + SheetPage.POWER -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + PowerEdit( + state = currentState.accelerometer, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.WRITE -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if (currentState is AccelerometerContract.State.Display) { + + currentState.writeState?.let { + + Write( + state = it, + onEvent = { + viewModel.setEvent(it) + } + ) + + } + + } + } + SheetPage.ACCEL_MODE_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelViewEdit( + next = AccelerometerContract.Event.Next.ACCEL, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.SPECTRE_MODE_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelViewEdit( + next = AccelerometerContract.Event.Next.SPECTRE, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.HISTORY_MODE_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelViewEdit( + next = AccelerometerContract.Event.Next.HISTORY, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.SPECTRE_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelSpectreEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.FREQUENCY_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelFrequencyEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.AXIS_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelFftAxisEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.FFT_MODE_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelFftModeEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.INTERVAL_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + IntervalEdit( + state = currentState.accelerometer, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.ACCEL_SCALE -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelScaleEdit( + next = AccelerometerContract.Event.Next.ACCEL, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.SPECTRE_SCALE -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelScaleEdit( + next = AccelerometerContract.Event.Next.SPECTRE, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.HISTORY_SCALE -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelScaleEdit( + next = AccelerometerContract.Event.Next.HISTORY, + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.ACCEL_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + AccelEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } + SheetPage.HISTORY_EDIT -> bottomDialog.show { + + val currentState = viewModel.viewState.value + + if(currentState is AccelerometerContract.State.Display) { + HistoryEdit( + state = currentState, + onEvent = { + viewModel.setEvent(it) + } + ) + } + + } null -> { bottomDialog.hide() } } } + DisposableEffect(key1 = Unit, effect = { + onDispose { + scope.launch { + bottomDialog.hide() + } + } + }) + LaunchedEffect("effect"){ viewModel.effect.onEach { when(it){ - AccelerometerContract.Effect.ShowAccelerometerMeasure -> { + is AccelerometerContract.Effect.ShowAccelerometerAccel -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.ACCEL + } + is AccelerometerContract.Effect.HidePowerPicker -> launch { + sheetPage = null + delay(100) + } + is AccelerometerContract.Effect.ShowPowerPicker -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.POWER + } + is AccelerometerContract.Effect.HideWriteBle -> launch { + sheetPage = null + delay(100) + } + is AccelerometerContract.Effect.ShowWriteBle -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.WRITE + } + is AccelerometerContract.Effect.ShowAccelerometerSpectre -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.SPECTRE + } + is AccelerometerContract.Effect.ShowAccelViewEdit -> launch { + sheetPage = null + delay(100) + sheetPage = when(it.next){ + AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_MODE_EDIT + AccelerometerContract.Event.Next.SPECTRE -> SheetPage.SPECTRE_MODE_EDIT + AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_MODE_EDIT + } + } + is AccelerometerContract.Effect.ShowSpectreEdit -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.SPECTRE_EDIT + } + is AccelerometerContract.Effect.ShowFftFrequencyEdit -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.FREQUENCY_EDIT + } + is AccelerometerContract.Effect.ShowFftAxisEdit -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.AXIS_EDIT + } + is AccelerometerContract.Effect.ShowFftModeEdit -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.FFT_MODE_EDIT + } + is AccelerometerContract.Effect.ShowAccelerometerHistory -> launch { + sheetPage = null + delay(100) + sheetPage = SheetPage.HISTORY + } + is AccelerometerContract.Effect.HideIntervalPicker -> { + sheetPage = null + delay(100) + } + is AccelerometerContract.Effect.ShowIntervalPicker -> { + sheetPage = null + delay(100) + sheetPage = SheetPage.INTERVAL_EDIT + } + is AccelerometerContract.Effect.ShowAccelScaleEdit -> { + sheetPage = null + delay(100) + sheetPage = when(it.next){ + AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_SCALE + AccelerometerContract.Event.Next.SPECTRE -> SheetPage.SPECTRE_SCALE + AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_SCALE + } + } + is AccelerometerContract.Effect.ShowAccelEdit -> { + sheetPage = null + delay(100) + sheetPage = SheetPage.ACCEL_EDIT + } + is AccelerometerContract.Effect.Navigation -> { + onEvent(it) + } + is AccelerometerContract.Effect.ShowHistoryEdit -> { + sheetPage = null + delay(100) + sheetPage = SheetPage.HISTORY_EDIT + } + is AccelerometerContract.Effect.HideHistoryEdit -> { sheetPage = null delay(100) - sheetPage = SheetPage.MEASURE_HISTORY } } }.launchIn(this) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerViewModel.kt index 34b09a9..44c40fd 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerViewModel.kt @@ -1,16 +1,26 @@ package llc.arma.ble.app.ui.screen.inspection.accelerometer +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.mapper.BleMapper +import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.model.BleView -import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.usecase.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.WriteBle import javax.inject.Inject @HiltViewModel class AccelerometerViewModel @Inject constructor( - private val bleMapper: BleMapper + private val bleMapper: BleMapper, + private val bleViewMapper: BleViewMapper, + private val writeBle: WriteBle ) : BaseViewModel() { override fun setInitialState() = AccelerometerContract.State.Loading @@ -18,29 +28,475 @@ 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) + is AccelerometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnHideAccelerometerSpectre -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnShowAccelerometerSpectre -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnAccelViewModeEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnAccelViewModelChanged -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnSpectreEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnFftFrequencyEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnFftAxisChanged -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnFftFrequencyChanged -> reduce(viewState.value, event) + 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) + is AccelerometerContract.Event.OnAccelScaleChanged -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnAccelScaleEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnAccelEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnHistoryEdit -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnHistoryScaleChanged -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnHistoryViewModeChanged -> reduce(viewState.value, event) + is AccelerometerContract.Event.OnHideHistoryEdit -> reduce(viewState.value, event) } } private fun reduce( state: AccelerometerContract.State, - event: AccelerometerContract.Event.OnHideAccelerometerMeasure - ) { - - - - } - - private fun reduce( - state: AccelerometerContract.State, - event: AccelerometerContract.Event.OnShowAccelerometerMeasure + event: AccelerometerContract.Event.OnHideHistoryEdit ) { setEffect { - AccelerometerContract.Effect.ShowAccelerometerMeasure + AccelerometerContract.Effect.HideHistoryEdit } + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHistoryViewModeChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + var saveHistory = state.accelerometer.accelerometerState.saveHistory + + if(saveHistory is Ble.Accelerometer.History.Enabled){ + saveHistory = Ble.Accelerometer.History.Enabled( + mode = event.mode, + scale = saveHistory.scale, + detailed = saveHistory.detailed + ) + } + + state.accelerometer.accelerometerState.saveHistory = saveHistory + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHistoryScaleChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + var saveHistory = state.accelerometer.accelerometerState.saveHistory + + if(saveHistory is Ble.Accelerometer.History.Enabled){ + saveHistory = saveHistory.copy(scale = event.scale) + } + + + state.accelerometer.accelerometerState.saveHistory = saveHistory + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHistoryEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowHistoryEdit + } + + } + + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnAccelScaleChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + setState { + state.copy( + accelScale = event.scale + ) + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnAccelScaleEdit + ) { + setEffect { + AccelerometerContract.Effect.ShowAccelScaleEdit( + event.next + ) + } + } + + 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) { + + if(event.save){ + + state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.History.Enabled( + scale = AccelScale.S_2, + mode = AccelViewMode.ACCELERATION, + detailed = true + ) + + setEffect { + AccelerometerContract.Effect.ShowHistoryEdit + } + + } else { + + state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.History.Disabled + + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftModeEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowFftModeEdit + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftAxisEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowFftAxisEdit + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftAxisChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + setState { + state.copy( + fftAxis = event.axis + ) + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftModeChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + setState { + state.copy( + fftViewMode = event.mode + ) + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftFrequencyChanged + ) { + + if(state is AccelerometerContract.State.Display){ + + setState { + state.copy( + fftFrequency = event.frequency + ) + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnFftFrequencyEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowFftFrequencyEdit + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnAccelEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowAccelEdit + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnSpectreEdit + ) { + + setEffect { + AccelerometerContract.Effect.ShowSpectreEdit + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnShowAccelerometerSpectre + ) { + + setEffect { + AccelerometerContract.Effect.ShowAccelerometerSpectre + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHideAccelerometerSpectre + ) { + + + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnShowAccelerometerHistory + ) { + + setEffect { + AccelerometerContract.Effect.ShowAccelerometerHistory + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHideAccelerometerHistory + ) { + + + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHideWriteBlePreview + ) { + + if(state is AccelerometerContract.State.Display){ + + setState { + state.copy( + writeState = null + ) + } + + } + + setEffect { + AccelerometerContract.Effect.HideWriteBle + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnShowWriteBlePreview + ) { + + if(state is AccelerometerContract.State.Display){ + + val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer + + val writeRequest = Ble.Accelerometer.WriteRequest( + tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx, + 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( + writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview( + writeRequest + ) + ) + } + + setEffect { + AccelerometerContract.Effect.ShowWriteBle + } + + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnPowerChanged + ) { + + if(state is AccelerometerContract.State.Display) { + + state.accelerometer.state.tx = event.tx + + } + + setEffect { + AccelerometerContract.Effect.HidePowerPicker + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnAccelViewModelChanged + ) { + + if(state is AccelerometerContract.State.Display) { + + setState { + state.copy( + accelViewMode = event.mode + ) + } + + } + + setEffect { + AccelerometerContract.Effect.HidePowerPicker + } + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnPowerEdit + ) { + setEffect { + AccelerometerContract.Effect.ShowPowerPicker + } + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnAccelViewModeEdit + ) { + setEffect { + AccelerometerContract.Effect.ShowAccelViewEdit(event.next) + } + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnHideAccelerometerAccel + ) { + + + + } + + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnShowAccelerometerAccel + ) { + + viewModelScope.launch { + + setEffect { + AccelerometerContract.Effect.ShowAccelerometerAccel + } + + } + + + } private fun reduce( @@ -52,7 +508,9 @@ class AccelerometerViewModel @Inject constructor( is AccelerometerContract.State.Display -> setState { state.copy( origin = Ble.Accelerometer( - info = event.ble.info + info = event.ble.info, + state = event.ble.state, + accelerometerState = state.origin.accelerometerState ) ) } @@ -60,11 +518,85 @@ class AccelerometerViewModel @Inject constructor( is AccelerometerContract.State.Loading -> setState { AccelerometerContract.State.Display( origin = event.ble, - accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer + accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer, + writeState = null, + accelViewMode = AccelViewMode.ACCELERATION, + fftAxis = FftAxis.AUTO, + fftFrequency = FftFrequency.F_400, + fftViewMode = FftViewMode.SPECTRE, + accelScale = AccelScale.S_2 ) } } } + private fun reduce( + state: AccelerometerContract.State, + event: AccelerometerContract.Event.OnWriteBle + ) { + + if(state is AccelerometerContract.State.Display){ + + state.writeState?.let { request -> + + if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) { + + viewModelScope.launch { + + setState { + state.copy( + writeState = AccelerometerContract.State.Display.WriteState.Writing( + request.writeRequest + ) + ) + } + + writeBle(state.accelerometer.info.serial, request.writeRequest).fold( + onSuccess = { + + val currentState = viewState.value + + if(currentState is AccelerometerContract.State.Display) { + + val newBleObject = Ble.Accelerometer( + info = currentState.origin.info, + state = currentState.origin.state.copy( + tx = request.writeRequest.tx ?: state.origin.state.tx + ), + accelerometerState = currentState.origin.accelerometerState.copy( + saveHistory = request.writeRequest.saveHistory + ?: currentState.origin.accelerometerState.saveHistory + ) + ) + + setState { + currentState.copy( + origin = newBleObject, + writeState = AccelerometerContract.State.Display.WriteState.Success + ) + } + + } + + }, + onFailure = { + setState { + state.copy( + writeState = AccelerometerContract.State.Display.WriteState.Failure + ) + } + } + ) + + } + + } + + } + + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelEdit.kt new file mode 100644 index 0000000..fb9f161 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelEdit.kt @@ -0,0 +1,172 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode + +@Composable +fun AccelEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + val accelMode = state.accelViewMode + val fftMode = state.fftViewMode + val fftAxis = state.fftAxis + val fftFrequency = state.fftFrequency + val accelScale = state.accelScale + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Ускорение", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + ) { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent( + AccelerometerContract.Event.OnAccelViewModeEdit( + next = AccelerometerContract.Event.Next.ACCEL + ) + ) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Accel view mode" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = accelMode.localized + ) + + } + + 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.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.ACCEL)) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Accel scale" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = accelScale.localized + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + } + + 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.OnShowAccelerometerAccel) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Продолжить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftAxisEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftAxisEdit.kt new file mode 100644 index 0000000..785444b --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftAxisEdit.kt @@ -0,0 +1,100 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +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 + +@Composable +fun AccelFftAxisEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var fftAxis = state.fftAxis + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Fft axis", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FftAxis.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onEvent(AccelerometerContract.Event.OnFftAxisChanged(it)) } + .padding(4.dp) + ) { + + RadioButton( + selected = it == fftAxis, + onClick = { + onEvent(AccelerometerContract.Event.OnFftAxisChanged(it)) + } + ) + + Text(text = it.localized) + + } + + } + + 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.OnSpectreEdit) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftModeEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftModeEdit.kt new file mode 100644 index 0000000..4636676 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftModeEdit.kt @@ -0,0 +1,100 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +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 + +@Composable +fun AccelFftModeEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var fftMode = state.fftViewMode + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Fft view mode", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FftViewMode.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onEvent(AccelerometerContract.Event.OnFftModeChanged(it)) } + .padding(4.dp) + ) { + + RadioButton( + selected = it == fftMode, + onClick = { + onEvent(AccelerometerContract.Event.OnFftModeChanged(it)) + } + ) + + Text(text = it.localized) + + } + + } + + 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.OnSpectreEdit) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFrequencyEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFrequencyEdit.kt new file mode 100644 index 0000000..73cc0af --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFrequencyEdit.kt @@ -0,0 +1,110 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +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 + +/*val AccelViewMode.localized: String + get() { + return when(this){ + ACCELERATION -> "Ускорение" + PEAK_ACCELERATION -> "Пиковое ускорение" + RMS -> "Среднеквадратичное ускорение" + ANGLE -> "Угол" + } + }*/ + +@Composable +fun AccelFrequencyEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var fftFrequency = state.fftFrequency + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Fft frequency", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FftFrequency.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it)) } + .padding(4.dp) + ) { + + RadioButton( + selected = it == fftFrequency, + onClick = { + onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it)) + } + ) + + Text(text = it.localized) + + } + + } + + 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.OnSpectreEdit) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelScaleEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelScaleEdit.kt new file mode 100644 index 0000000..1402fc0 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelScaleEdit.kt @@ -0,0 +1,138 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +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 + +@Composable +fun AccelScaleEdit( + next: AccelerometerContract.Event.Next, + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var fftMode = when(next){ + AccelerometerContract.Event.Next.ACCEL, + AccelerometerContract.Event.Next.SPECTRE -> + state.accelScale + AccelerometerContract.Event.Next.HISTORY -> { + val history = state.accelerometer.accelerometerState.saveHistory + if (history is Ble.Accelerometer.History.Enabled) + history.scale + else { + state.accelScale + } + } + } + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Accel scale", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + AccelScale.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { + when(next){ + AccelerometerContract.Event.Next.ACCEL, + AccelerometerContract.Event.Next.SPECTRE -> + onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it)) + AccelerometerContract.Event.Next.HISTORY -> + onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it)) + } + } + .padding(4.dp) + ) { + + RadioButton( + selected = it == fftMode, + onClick = { + when(next){ + AccelerometerContract.Event.Next.ACCEL, + AccelerometerContract.Event.Next.SPECTRE -> + onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it)) + AccelerometerContract.Event.Next.HISTORY -> + onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it)) + } + + } + ) + + Text(text = it.localized) + + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + when(next){ + AccelerometerContract.Event.Next.ACCEL -> + onEvent(AccelerometerContract.Event.OnAccelEdit) + AccelerometerContract.Event.Next.SPECTRE -> + onEvent(AccelerometerContract.Event.OnSpectreEdit) + AccelerometerContract.Event.Next.HISTORY -> + onEvent(AccelerometerContract.Event.OnHistoryEdit) + } + + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelSpectreEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelSpectreEdit.kt new file mode 100644 index 0000000..a56b76e --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelSpectreEdit.kt @@ -0,0 +1,294 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode + +val FftFrequency.localized: String + get() { + return when(this){ + FftFrequency.OFF -> "откл" + FftFrequency.F_1 -> "1 Гц" + FftFrequency.F_10 -> "10 Гц" + FftFrequency.F_25 -> "25 Гц" + FftFrequency.F_50 -> "50 Гц" + FftFrequency.F_100 -> "100 Гц" + FftFrequency.F_200 -> "200 Гц" + FftFrequency.F_400 -> "400 Гц" + FftFrequency.F_1620 -> "1620 Гц" + FftFrequency.F_1344 -> "1344 Гц" + } + } + +val FftAxis.localized: String + get() { + return when(this){ + FftAxis.AUTO -> "Авто" + FftAxis.X -> "Ось X" + FftAxis.Y -> "Ось Y" + FftAxis.Z -> "Ось Z" + } + } + +val FftViewMode.localized: String + get() { + return when(this){ + FftViewMode.SPECTRE -> "Спектр" + FftViewMode.X -> "Ось X" + FftViewMode.Y -> "Ось Y" + FftViewMode.Z -> "Ось Z" + } + } + +val AccelScale.localized: String + get() { + return when(this){ + AccelScale.S_2 -> "2g" + AccelScale.S_4 -> "4g" + AccelScale.S_8 -> "8g" + AccelScale.S_16 -> "16g" + } + } + +@Composable +fun AccelSpectreEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + val accelMode = state.accelViewMode + val fftMode = state.fftViewMode + val fftAxis = state.fftAxis + val fftFrequency = state.fftFrequency + val accelScale = state.accelScale + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Спектр", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + ) { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(AccelerometerContract.Event.OnFftModeEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Fft view mode" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = fftMode.localized + ) + + } + + 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.OnFftAxisEdit) } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Fft axis" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = fftAxis.localized + ) + + } + + 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.OnFftFrequencyEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Fft frequency" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = fftFrequency.localized + ) + + } + + 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.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.SPECTRE)) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Accel scale" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = accelScale.localized + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + } + + 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.OnShowAccelerometerSpectre) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Продолжить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelViewEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelViewEdit.kt new file mode 100644 index 0000000..0cf428f --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelViewEdit.kt @@ -0,0 +1,124 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.domain.usecase.AccelViewMode +import llc.arma.ble.domain.usecase.AccelViewMode.* + +val AccelViewMode.localized: String + get() { + return when(this){ + ACCELERATION -> "Ускорение" + PEAK_ACCELERATION -> "Пиковое ускорение" + RMS -> "Среднеквадратичное ускорение" + VIBRATION -> "Вибрация" + ANGLE -> "Угол" + } + } + +@Composable +fun AccelViewEdit( + next: AccelerometerContract.Event.Next, + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var value by remember(state.accelViewMode) { + mutableStateOf(state.accelViewMode) + } + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Accel view mode", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { value = it } + .padding(4.dp) + ) { + + RadioButton( + selected = it == value, + onClick = { + value = it + + } + ) + + Text(text = it.localized) + + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + + when(next){ + AccelerometerContract.Event.Next.ACCEL -> { + onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value)) + onEvent(AccelerometerContract.Event.OnAccelEdit) + } + AccelerometerContract.Event.Next.SPECTRE -> { + onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value)) + onEvent(AccelerometerContract.Event.OnSpectreEdit) + } + + AccelerometerContract.Event.Next.HISTORY -> { + onEvent(AccelerometerContract.Event.OnHistoryViewModeChanged(value)) + onEvent(AccelerometerContract.Event.OnHistoryEdit) + } + } + + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelerometerSpectre.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelerometerSpectre.kt new file mode 100644 index 0000000..7de1d1c --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelerometerSpectre.kt @@ -0,0 +1,500 @@ +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() + 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 { 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()) + } + + 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?, + val loadingHistoryState : ProgressState> + ) : State() + + object Exception : State() + + } + + sealed class Effect : ViewSideEffect { + + } + +} + +@HiltViewModel +class AccelerometerSpectreViewModel @Inject constructor( + private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial +) : BaseViewModel() { + + 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() + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterAccel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterAccel.kt new file mode 100644 index 0000000..e05195c --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterAccel.kt @@ -0,0 +1,596 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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.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.draw.rotate +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.style.TextAlign +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.core.chart.decoration.ThresholdLine +import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp +import com.patrykandpatrick.vico.core.entry.FloatEntry +import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition +import com.patrykandpatrick.vico.core.scroll.InitialScroll +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode +import llc.arma.ble.domain.usecase.MeasureData +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.GetAccelerometerMeasureBySerialFlow + +@Composable +fun AccelerometerAccel( + ble: BleInfo, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency +) { + + val viewModel = hiltViewModel() + val state = viewModel.viewState.value + + viewModel.setEvent(AccelerometerAccelContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) + + DisposableEffect(key1 = "ble", effect = { + + onDispose { + viewModel.setEvent(AccelerometerAccelContract.Event.StopMeasure) + } + + }) + + Column( + modifier = Modifier.fillMaxHeight(0.9f) + ) { + + Row( + modifier = Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + modifier = Modifier.weight(1f), + text = accelMode.localized, + style = MaterialTheme.typography.titleLarge + ) + + IconButton( + onClick = { + viewModel.setEvent(AccelerometerAccelContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) + }, + enabled = true + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Box(modifier = Modifier) { + + when (state) { + is AccelerometerAccelContract.State.Display -> Display(state = state) + is AccelerometerAccelContract.State.Exception -> Exception() + } + + } + + } + +} + + +@Composable +fun Display( + state: AccelerometerAccelContract.State.Display +) { + + Box(modifier = Modifier + .padding(8.dp) + .fillMaxSize() + ) { + + + if (state.measureHistory.isEmpty()) { + + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + strokeCap = StrokeCap.Round + ) + + } else { + + + val xProducer = remember { + ChartEntryModelProducer(listOf()) + } + + val yProducer = remember { + ChartEntryModelProducer(listOf()) + } + + val zProducer = remember { + ChartEntryModelProducer(listOf()) + } + + xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> + when(measurePoint){ + is MeasureData.Accelerate -> { + FloatEntry(index.toFloat(), measurePoint.x ) + } + is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value) + is MeasureData.Angle -> { + FloatEntry(index.toFloat(), measurePoint.xAngle ) + } + } + }) + + yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> + when(measurePoint){ + is MeasureData.Accelerate -> { + FloatEntry(index.toFloat(), measurePoint.y ) + } + is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value) + is MeasureData.Angle -> { + FloatEntry(index.toFloat(), measurePoint.yAngle ) + } + } + }) + + zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> + when(measurePoint){ + is MeasureData.Accelerate -> { + FloatEntry(index.toFloat(), measurePoint.z ) + } + is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value) + is MeasureData.Angle -> { + FloatEntry(index.toFloat(), measurePoint.zAngle ) + } + } + }) + + val lineChart = lineChart( + decorations = listOf( + ThresholdLine( + thresholdValue = 0f + ) + ) + ) + + val lastMeasure = state.measureHistory.lastOrNull() + + if(lastMeasure is MeasureData.Accelerate) { + + if(state.mode == AccelViewMode.ANGLE){ + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Ось X: ${lastMeasure.x}") + + Spacer(modifier = Modifier.height(8.dp)) + + Angle( + angle = lastMeasure.x, + modifier = Modifier.weight(1f), + ) + + } + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Ось Y: ${lastMeasure.y}") + + Spacer(modifier = Modifier.height(8.dp)) + + Angle( + modifier = Modifier.weight(1f), + angle = lastMeasure.y + ) + + } + + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Ось Z: ${lastMeasure.z}") + + Spacer(modifier = Modifier.height(8.dp)) + + Angle( + modifier = Modifier.weight(1f), + angle = lastMeasure.z + ) + + } + + } + + } else { + + Column() { + + Text(text = "Ось X:") + + Chart( + chart = lineChart, + chartModelProducer = xProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + Text(text = "Ось Y:") + Chart( + chart = lineChart, + chartModelProducer = yProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + Text(text = "Ось Z:") + Chart( + chart = lineChart, + chartModelProducer = zProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + } + + } + + } else { + + Column { + + Text(text = "Вибрация:") + + Chart( + chart = lineChart, + chartModelProducer = xProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + } + + } + + } + + } +} + +@Composable +public fun Angle( + modifier: Modifier = Modifier, + angle: Float +) { + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer + ) { + + Column( + modifier = modifier.padding(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "0°C") + + Row( + modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + + Text(text = "-90°C") + + Surface( + modifier = Modifier + .aspectRatio(1f) + .fillMaxHeight() + .padding(8.dp), + color = MaterialTheme.colorScheme.secondary, + shape = CircleShape + ) { + + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .rotate(-90f + angle), + horizontalArrangement = Arrangement.End + ) { + + Box(modifier = Modifier.weight(1f)) + + Divider(modifier = Modifier.weight(1f)) + + } + + } + + } + + Text(text = "90°C") + + } + + Text(text = "±180°C") + + } + + } + +} + +@Composable +private fun Exception( + +) { + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .aspectRatio(2f), + ){ + + Text( + textAlign = TextAlign.Center, + text = "Во время загрузки произошла ошибка", + modifier = Modifier.align(Alignment.Center) + ) + + } + +} + +class AccelerometerAccelContract { + + sealed class Event : ViewEvent { + + object StopMeasure : Event() + + data class OnStart( + val serial: String, + val accelScale: AccelScale, + val accelMode: AccelViewMode, + val fftAxis: FftAxis, + val fftMode: FftViewMode, + 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 + ) : Event() + + } + + sealed class State : ViewState { + + data class Display( + val mode: AccelViewMode, + val measureHistory : List + ) : State() + + object Exception : State() + + } + + sealed class Effect : ViewSideEffect { + + } + +} + + + +@HiltViewModel +class AccelerometerAccelViewModel @Inject constructor( + private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow, +) : BaseViewModel() { + + var measureJob: Job? = null + + private var lastSerial: String? = null + + override fun setInitialState() = AccelerometerAccelContract.State.Display( + mode = AccelViewMode.ACCELERATION, + measureHistory = emptyList() + ) + + override fun handleEvents(event: AccelerometerAccelContract.Event) { + when(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: AccelerometerAccelContract.State, + event: AccelerometerAccelContract.Event.StopMeasure + ) { + + measureJob?.cancel() + measureJob = null + + setState { + AccelerometerAccelContract.State.Display( + mode = AccelViewMode.ACCELERATION, + measureHistory = emptyList() + ) + } + + } + + private fun reduce( + state: AccelerometerAccelContract.State, + event: AccelerometerAccelContract.Event.OnStart + ) { + + startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, false) + + } + + private fun reduce( + state: AccelerometerAccelContract.State, + event: AccelerometerAccelContract.Event.OnRefreshHistory + ) { + startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, true) + } + + private fun startReadMeasure( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency, + restartJob: Boolean + ){ + + if(restartJob || measureJob == null) { + measureJob?.cancel() + measureJob = null + measureJob = viewModelScope.launch { + + setState { + AccelerometerAccelContract.State.Display( + mode = AccelViewMode.ACCELERATION, + measureHistory = emptyList() + ) + } + + getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency).onEach { + it.fold( + onSuccess = { + setState { + when (this) { + is AccelerometerAccelContract.State.Display -> { + var dataList = this.measureHistory.toMutableList().apply { + add(it) + } + if(accelMode != AccelViewMode.ANGLE) { + dataList = dataList.takeLast(10).toMutableList() + } + AccelerometerAccelContract.State.Display(accelMode, dataList) + } + + AccelerometerAccelContract.State.Exception -> { + AccelerometerAccelContract.State.Display(accelMode, listOf(it)) + } + } + } + }, + onFailure = { + setState { + AccelerometerAccelContract.State.Exception + } + } + ) + }.launchIn(this) + + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterHistory.kt new file mode 100644 index 0000000..a31c832 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterHistory.kt @@ -0,0 +1,589 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.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.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.CloudUpload +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.style.TextAlign +import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec +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.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.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.ExportToXlsx +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.GetAccelerometerHistoryBySerial +import llc.arma.ble.domain.usecase.MeasureData +import java.util.Date + +class AccelEntry( + val localDate: Long, + override val x: Float, + override val y: Float, +) : ChartEntry { + + override fun withY(y: Float) = AccelEntry(localDate, x, y) + +} + +@Composable +fun AccelerometerHistory( + ble: BleInfo, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency +) { + + val viewModel = hiltViewModel() + val state = viewModel.viewState.value + + LaunchedEffect(ble.serial) { + viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) + } + + /*DisposableEffect("ble") { + onDispose { + viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure) + } + }*/ + + Column( + modifier = Modifier.fillMaxHeight(0.9f) + ) { + + Row( + modifier = Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + val title = when(state){ + is AccelerometerHistoryContract.State.Display -> { + when (state.loadingHistoryState) { + is ProgressState.Finished -> "График измерений (${state.loadingHistoryState.data.size})" + is ProgressState.Indeterminate -> "График измерений" + is ProgressState.Progress -> "График измерений" + } + } + AccelerometerHistoryContract.State.Exception -> "График измерений" + } + + Text( + modifier = Modifier.weight(1f), + text = title, + style = MaterialTheme.typography.titleLarge + ) + + IconButton( + onClick = { + viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport) + }, + enabled = when(state){ + is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished + AccelerometerHistoryContract.State.Exception -> false + } + ) { + Icon( + imageVector = Icons.Rounded.CloudUpload, + contentDescription = null + ) + } + + IconButton( + onClick = { + viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(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)) + + Box(modifier = Modifier) { + + when (state) { + is AccelerometerHistoryContract.State.Display -> Display(state = state) + is AccelerometerHistoryContract.State.Exception -> Exception() + } + + } + + } + +} + + +@Composable +fun Display( + state: AccelerometerHistoryContract.State.Display +) { + + Box(modifier = Modifier + .padding(8.dp) + .fillMaxSize() + ) { + + when (state.loadingHistoryState) { + + is ProgressState.Finished -> { + + if(state.loadingHistoryState.data.isEmpty()){ + + Text( + modifier = Modifier.align(Alignment.Center), + text = "Нет данных" + ) + + } else { + + val axisValueFormatter = + AxisValueFormatter { value, chartValues -> + (chartValues.chartEntryModel.entries.firstOrNull() + ?.getOrNull(value.toInt()) as? AccelEntry) + ?.localDate + ?.let { formatter.format(Date(it)) } + .orEmpty() + } + + val xProducer = remember { + ChartEntryModelProducer(listOf()) + } + + val yProducer = remember { + ChartEntryModelProducer(listOf()) + } + + val zProducer = remember { + ChartEntryModelProducer(listOf()) + } + + xProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint -> + when(measurePoint){ + is Ble.Accelerometer.HistoryPoint.Accelerate -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.x ) + } + is Ble.Accelerometer.HistoryPoint.Vibration -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value) + } + is Ble.Accelerometer.HistoryPoint.Angle -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.x ) + } + } + }) + + yProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint -> + when(measurePoint){ + is Ble.Accelerometer.HistoryPoint.Accelerate -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.y ) + } + is Ble.Accelerometer.HistoryPoint.Vibration -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value) + } + is Ble.Accelerometer.HistoryPoint.Angle -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.y ) + } + } + }) + + zProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint -> + when(measurePoint){ + is Ble.Accelerometer.HistoryPoint.Accelerate -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.z ) + } + is Ble.Accelerometer.HistoryPoint.Vibration -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value) + } + is Ble.Accelerometer.HistoryPoint.Angle -> { + AccelEntry(measurePoint.date, index.toFloat(), measurePoint.z ) + } + } + }) + + val lineChart = lineChart( + decorations = listOf( + ThresholdLine( + thresholdValue = 0f + ) + ) + ) + + val lastMeasure = state.loadingHistoryState.data.lastOrNull() + + if((lastMeasure is Ble.Accelerometer.HistoryPoint.Vibration).not()) { + + Column() { + + Text(text = "Ось X:") + + Chart( + chart = lineChart, + chartModelProducer = xProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + tickLength = 0.dp, + valueFormatter = axisValueFormatter, + labelRotationDegrees = -90f, + ), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + Text(text = "Ось Y:") + Chart( + chart = lineChart, + chartModelProducer = yProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + tickLength = 0.dp, + valueFormatter = axisValueFormatter, + labelRotationDegrees = -90f, + ), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + Text(text = "Ось Z:") + Chart( + chart = lineChart, + chartModelProducer = zProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + tickLength = 0.dp, + valueFormatter = axisValueFormatter, + labelRotationDegrees = -90f, + ), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + } + + } else { + + Column { + + Text(text = "Вибрация:") + + Chart( + chart = lineChart, + chartModelProducer = xProducer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + tickLength = 0.dp, + valueFormatter = axisValueFormatter, + labelRotationDegrees = -90f, + ), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + autoScaleUp = AutoScaleUp.None, + diffAnimationSpec = tween(0), + chartScrollSpec = rememberChartScrollSpec( + initialScroll = InitialScroll.End, + autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased, + autoScrollAnimationSpec = tween(0) + ) + ) + + } + + } + + } + + } + 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() { + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .aspectRatio(2f), + ){ + + Text( + textAlign = TextAlign.Center, + text = "Во время загрузки произошла ошибка", + modifier = Modifier.align(Alignment.Center) + ) + + } + +} + +class AccelerometerHistoryContract { + + sealed class Event : ViewEvent { + + object StopMeasure : Event() + + object OnExport : Event() + + data class OnStart( + val serial: String, + val accelScale: AccelScale, + val accelMode: AccelViewMode, + val fftAxis: FftAxis, + val fftMode: FftViewMode, + 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 + ) : Event() + + } + + sealed class State : ViewState { + + data class Display( + val loadingHistoryState : ProgressState> + ) : State() + + object Exception : State() + + } + + sealed class Effect : ViewSideEffect { + + } + +} + + + +@HiltViewModel +class AccelerometerHistoryViewModel @Inject constructor( + private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial, + private val exportToXlsx: ExportToXlsx +) : BaseViewModel() { + + var measureJob: Job? = null + + private var lastSerial: String? = null + + override fun setInitialState() = AccelerometerHistoryContract.State.Display( + ProgressState.Indeterminate + ) + + override fun handleEvents(event: AccelerometerHistoryContract.Event) { + when(event){ + is AccelerometerHistoryContract.Event.OnStart -> reduce(viewState.value, event) + is AccelerometerHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event) + is AccelerometerHistoryContract.Event.StopMeasure -> reduce(viewState.value, event) + is AccelerometerHistoryContract.Event.OnExport -> reduce(viewState.value, event) + } + } + + private fun reduce( + state: AccelerometerHistoryContract.State, + event: AccelerometerHistoryContract.Event.OnExport + ) { + + if(state is AccelerometerHistoryContract.State.Display){ + if(state.loadingHistoryState is ProgressState.Finished){ + exportToXlsx.invoke(state.loadingHistoryState.data) + } + } + + } + + 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 + ) { + + viewModelScope.launch { + + if(state is AccelerometerHistoryContract.State.Display) { + + if(lastSerial != event.serial) { + + lastSerial = event.serial + + 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) + + } + + } + + } + + } + + private fun reduce( + state: AccelerometerHistoryContract.State, + event: AccelerometerHistoryContract.Event.OnRefreshHistory + ) { + viewModelScope.launch { + + 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) + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterMeasure.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterMeasure.kt deleted file mode 100644 index 442d900..0000000 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterMeasure.kt +++ /dev/null @@ -1,301 +0,0 @@ -package llc.arma.ble.app.ui.screen.inspection.accelerometer.view - -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.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.text.style.TextAlign -import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis -import com.patrykandpatrick.vico.compose.chart.column.columnChart -import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec -import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp -import com.patrykandpatrick.vico.core.entry.FloatEntry -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.domain.usecase.GetAccelerometerMeasureBySerialFlow - -@Composable -fun AccelerometerMeasure( - ble: BleInfo -) { - - val viewModel = hiltViewModel() - val state = viewModel.viewState.value - - LaunchedEffect(ble.serial) { - viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial)) - } - - DisposableEffect(key1 = ble.serial, effect = { - onDispose { - viewModel.setEvent(AccelerometerMeasureContract.Event.StopMeasure) - } - }) - - Column( - modifier = Modifier.fillMaxHeight(0.9f) - ) { - - Row( - modifier = Modifier.padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - - val title = when(state){ - is AccelerometerMeasureContract.State.Display -> { - "История измерений" - } - AccelerometerMeasureContract.State.Exception -> "История измерений" - } - - Text( - modifier = Modifier.weight(1f), - text = title, - style = MaterialTheme.typography.titleLarge - ) - - IconButton( - onClick = { - viewModel.setEvent(AccelerometerMeasureContract.Event.OnRefreshHistory(ble.serial)) - }, - enabled = true - ) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - } - - } - - Spacer(modifier = Modifier.height(16.dp)) - - Box(modifier = Modifier) { - - when (state) { - is AccelerometerMeasureContract.State.Display -> Display(state = state) - AccelerometerMeasureContract.State.Exception -> Exception() - } - - } - - } - -} - - -@Composable -fun Display( - state: AccelerometerMeasureContract.State.Display -) { - - Box(modifier = Modifier - .padding(8.dp) - .fillMaxSize() - ) { - - - if (state.measureHistory.isEmpty()) { - - Text( - modifier = Modifier.align(Alignment.Center), - text = "Нет данных" - ) - - } else { - - val producer = remember { - ChartEntryModelProducer(listOf()) - } - - producer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> - FloatEntry(index.toFloat(), measurePoint) - }) - - val lineChart = columnChart() - - Chart( - chart = lineChart, - chartModelProducer = producer, - startAxis = startAxis(), - bottomAxis = bottomAxis(), - modifier = Modifier.fillMaxSize(), - autoScaleUp = AutoScaleUp.None, - diffAnimationSpec = tween(0), - chartScrollSpec = rememberChartScrollSpec( - initialScroll = InitialScroll.End, - autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased - ) - ) - - } - - - } -} - -@Composable -fun Exception( - -) { - Box( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth() - .aspectRatio(2f), - ){ - - Text( - textAlign = TextAlign.Center, - text = "Во время загрузки произошла ошибка", - modifier = Modifier.align(Alignment.Center) - ) - - } - -} - -class AccelerometerMeasureContract { - - sealed class Event : ViewEvent { - - object StopMeasure : Event() - - data class OnStart( - val serial: String - ) : Event() - - data class OnRefreshHistory( - val serial: String - ) : Event() - - } - - sealed class State : ViewState { - - data class Display( - val measureHistory : List - ) : State() - - object Exception : State() - - } - - sealed class Effect : ViewSideEffect { - - } - -} - - - -@HiltViewModel -class AccelerometerHistoryViewModel @Inject constructor( - private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow, -) : BaseViewModel() { - - var measureJob: Job? = null - - private var lastSerial: String? = null - - override fun setInitialState() = AccelerometerMeasureContract.State.Display( - emptyList() - ) - - override fun handleEvents(event: AccelerometerMeasureContract.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) - } - } - - private fun reduce( - state: AccelerometerMeasureContract.State, - event: AccelerometerMeasureContract.Event.StopMeasure - ) { - - measureJob?.cancel() - setState { - AccelerometerMeasureContract.State.Display(emptyList()) - } - - } - - private fun reduce( - state: AccelerometerMeasureContract.State, - event: AccelerometerMeasureContract.Event.OnStart - ) { - - startReadMeasure(event.serial, false) - - } - - private fun reduce( - state: AccelerometerMeasureContract.State, - event: AccelerometerMeasureContract.Event.OnRefreshHistory - ) { - startReadMeasure(event.serial, true) - } - - private fun startReadMeasure(serial: String, restartJob: Boolean){ - - if(restartJob || measureJob == null) { - measureJob = viewModelScope.launch { - - setState { - AccelerometerMeasureContract.State.Display(emptyList()) - } - - getAccelerometerMeasureBySerialFlow(serial).onEach { - it.fold( - onSuccess = { - setState { - when (this) { - is AccelerometerMeasureContract.State.Display -> { - val dataList = this.measureHistory.toMutableList().apply { - add(it) - } - AccelerometerMeasureContract.State.Display(dataList) - } - - AccelerometerMeasureContract.State.Exception -> { - AccelerometerMeasureContract.State.Display(listOf(it)) - } - } - } - }, - onFailure = { - setState { - AccelerometerMeasureContract.State.Exception - } - } - ) - }.launchIn(this) - - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt index 1da0541..5fc7039 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowRight -import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -62,7 +61,146 @@ fun DisplayState( modifier = Modifier .clip(RoundedCornerShape(16.dp)) .clickable { - onEvent(AccelerometerContract.Event.OnShowAccelerometerMeasure) + onEvent(AccelerometerContract.Event.OnPowerEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Мощность" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${ble.state.tx.value} db" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Сохранять историю измерений" + ) + + val history = ble.accelerometerState.saveHistory + + if(history is Ble.Accelerometer.History.Enabled){ + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "View mode ${history.mode.localized}" + ) + + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "Scale: ${history.scale.localized}" + ) + + } + + } + + Switch( + checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.History.Enabled, + 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) ) { @@ -86,6 +224,112 @@ 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.OnAccelEdit) } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Ускорение" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowRight, + contentDescription = null + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(AccelerometerContract.Event.OnSpectreEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Спектр" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowRight, + contentDescription = null + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(AccelerometerContract.Event.OnChangePassword) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Изменить пароль" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowRight, + contentDescription = null + ) + + } + + } + } ) @@ -99,7 +343,7 @@ fun DisplayState( shape = CircleShape, color = MaterialTheme.colorScheme.primaryContainer, onClick = { - + onEvent(AccelerometerContract.Event.OnShowWriteBlePreview) } ) { diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/HistoryEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/HistoryEdit.kt new file mode 100644 index 0000000..6d6aa6e --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/HistoryEdit.kt @@ -0,0 +1,174 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode + +@Composable +fun HistoryEdit( + state: AccelerometerContract.State.Display, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + val history = state.accelerometer.accelerometerState.saveHistory + + val detailed = if (history is Ble.Accelerometer.History.Enabled) history.detailed else false + val accelMode = if (history is Ble.Accelerometer.History.Enabled) history.mode else state.accelViewMode + val accelScale = if (history is Ble.Accelerometer.History.Enabled) history.scale else state.accelScale + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "История измерений", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + ) { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent( + AccelerometerContract.Event.OnAccelViewModeEdit( + next = AccelerometerContract.Event.Next.HISTORY + ) + ) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Accel view mode" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = accelMode.localized + ) + + } + + 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.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.HISTORY)) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Accel scale" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = accelScale.localized + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + } + + 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.OnHideHistoryEdit) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/IntervalEdit.kt new file mode 100644 index 0000000..e58a4c8 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/IntervalEdit.kt @@ -0,0 +1,225 @@ +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) { + mutableIntStateOf((state.accelerometerState.historyInterval).toInt()) + } + + val maxInterval = 10 * 24 * 60 * 60 * 1000 + + if(value > maxInterval){ + value = maxInterval + } + + if(value < 1){ + value = 1 * 60 * 1000 + } + + val maxMinutes = maxInterval / millisInMinute + val maxHours = maxInterval / millisInHour + val maxDays = maxInterval / millisInDay + + val dayValue = value / millisInDay + val hourValue = (value - (dayValue * millisInDay)) / millisInHour + val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute + + 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 * millisInDay) + (hourValue * millisInHour) + (minutesValue * millisInMinute) + } + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text(text = "Дни") + + Spacer(modifier = Modifier.width(16.dp)) + + NumberPicker( + range = -1..maxHours, + value = hourValue, + onValueChanged = { + value = (it * millisInHour) + (dayValue * millisInDay) + (minutesValue * millisInMinute) + } + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text(text = "Часы") + + Spacer(modifier = Modifier.width(16.dp)) + + NumberPicker( + range = -1..maxMinutes, + value = minutesValue, + onValueChanged = { + value = (it * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour) + } + ) + + 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() + ) + ) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Применить" + ) + + } + + } + + } + +} + +const val millisInMinute = 1000 * 60 +const val millisInHour = millisInMinute * 60 +const val millisInDay = millisInHour * 24 + + +@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 + ) + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/PowerEdit.kt new file mode 100644 index 0000000..2a7503e --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/PowerEdit.kt @@ -0,0 +1,97 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract + +@Composable +fun PowerEdit( + state: BleView.Accelerometer, + onEvent: (AccelerometerContract.Event) -> Unit, +){ + + var value by remember(state.state.tx) { + mutableStateOf(state.state.tx) + } + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Мощность", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + BleView.BleState.TX.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { value = it } + .padding(4.dp) + ) { + + RadioButton( + selected = it == value, + onClick = { value = it } + ) + + Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)") + + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent( + AccelerometerContract.Event.OnPowerChanged( + value + ) + ) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Применить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/Write.kt new file mode 100644 index 0000000..c8f88bd --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/Write.kt @@ -0,0 +1,431 @@ +package llc.arma.ble.app.ui.screen.inspection.accelerometer.view + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import llc.arma.ble.R +import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract +import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract +import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName +import llc.arma.ble.domain.model.Ble + +@Composable +fun Write( + state: AccelerometerContract.State.Display.WriteState, + onEvent: (AccelerometerContract.Event) -> Unit +) { + + Column( + modifier = Modifier.animateContentSize() + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Запись изменений", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(20.dp)) + + when (state) { + is AccelerometerContract.State.Display.WriteState.DisplayPreview -> { + + if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) { + + state.writeRequest.tx?.let { + Box( + modifier = Modifier.padding( + vertical = 0.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Мощность" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${it.localizedName} db" + ) + + } + + } + + } + } + + 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 = when(it){ + Ble.Accelerometer.History.Disabled -> "Выключено" + is Ble.Accelerometer.History.Enabled -> "Включено" + } + ) + + } + + } + + } + + } + + 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) + ) { + + val hours = it / 1000 / 60 / 60 + val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60 + + Text( + text = "Интервал измерений" + ) + + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "$hours ч. $minutes мин." + ) + + } + + } + + } + + } + + Spacer(modifier = Modifier.height(20.dp)) + + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent(AccelerometerContract.Event.OnWriteBle) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Записать" + ) + + } + + } + + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + onClick = { + onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) + }, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + text = "Отменить" + ) + + } + + } + + } else { + + Spacer(modifier = Modifier.height(38.dp)) + + Text( + text = "Нет изменений", + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(64.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + onClick = { + onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + + + } + is AccelerometerContract.State.Display.WriteState.Writing -> { + + Box { + + Column() { + + Spacer(modifier = Modifier.height(28.dp)) + + CircularProgressIndicator( + strokeCap = StrokeCap.Round, + modifier = Modifier + .align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + onClick = { + onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + text = "Отменить" + ) + + } + + } + + } + + } + + } + AccelerometerContract.State.Display.WriteState.Success -> { + + Box { + + Column { + + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + + Image( + modifier = Modifier + .size(125.dp) + .align(Alignment.Center), + painter = painterResource(llc.arma.ble.R.drawable.ic_done), + contentDescription = null + ) + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = "Успешно завершено" + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + onClick = { + onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + + } + + } + AccelerometerContract.State.Display.WriteState.Failure -> { + + Box { + + Column { + + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + + Image( + modifier = Modifier + .size(125.dp) + .align(Alignment.Center), + painter = painterResource(R.drawable.ic_error), + contentDescription = null + ) + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = "Ошибка записи" + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + onClick = { + onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + + } + + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/PowerEdit.kt index 7adf422..c3d21ec 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/PowerEdit.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/PowerEdit.kt @@ -55,7 +55,7 @@ fun PowerEdit( onClick = { value = it } ) - Text(text = it.value.toString() + " db") + Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)") } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerScreen.kt index 465efe2..da5ccc7 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerScreen.kt @@ -52,7 +52,6 @@ val Ble.BleState.TX.localizedName: String } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable fun ThermometerScreen( ble: Ble.Thermometer, diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/PowerEdit.kt index cb38f26..ca82507 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/PowerEdit.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/PowerEdit.kt @@ -54,7 +54,7 @@ fun PowerEdit( onClick = { value = it } ) - Text(text = it.value.toString() + " db") + Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)") } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/TemperatureHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/TemperatureHistory.kt index 0fd548a..cc72e9e 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/TemperatureHistory.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/TemperatureHistory.kt @@ -204,7 +204,7 @@ fun Display( animationSpec = tween( durationMillis = progressAnimDuration, easing = FastOutSlowInEasing - ) + ), label = "" ) CircularProgressIndicator( @@ -221,9 +221,7 @@ fun Display( } @Composable -fun Exception( - -) { +private fun Exception() { Box( modifier = Modifier .padding(8.dp) diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/Write.kt index 7bb8f81..fd506bf 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/Write.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/Write.kt @@ -106,7 +106,7 @@ fun Write( Text( color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.bodyMedium, - text = "${it.localizedName}" + text = it.localizedName ) } @@ -137,13 +137,17 @@ fun Write( modifier = Modifier.weight(1f) ) { + val hours = it / 1000 / 60 / 60 + val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60 + Text( - text = "Интервал измерний" + text = "Интервал измерений" ) + Text( color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.bodyMedium, - text = "${it / 1000 / 60 / 60} ч." + text = "$hours ч. $minutes мин." ) } diff --git a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt index 03aff5b..ffafb68 100644 --- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt +++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt @@ -11,7 +11,6 @@ import android.os.Build import android.os.SystemClock import android.util.Log import androidx.core.app.ActivityCompat -import androidx.core.util.toRange import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -24,17 +23,80 @@ import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.ConnectedBleInfo import llc.arma.ble.domain.repository.BleRepository +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode +import llc.arma.ble.domain.usecase.MeasureData +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode import java.util.* import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume -import kotlin.random.Random + + +val FftFrequency.sendData: Byte + get() { + return when(this){ + FftFrequency.OFF -> 0 + FftFrequency.F_1 -> 1 + FftFrequency.F_10 -> 2 + FftFrequency.F_25 -> 3 + FftFrequency.F_50 -> 4 + FftFrequency.F_100 -> 5 + FftFrequency.F_200 -> 6 + FftFrequency.F_400 -> 7 + FftFrequency.F_1620 -> 8 + FftFrequency.F_1344 -> 9 + } + } + +val FftAxis.sendData: Byte + get() { + return when(this){ + FftAxis.AUTO -> 0 + FftAxis.X -> 1 + FftAxis.Y -> 2 + FftAxis.Z -> 3 + } + } + +val FftViewMode.sendData: Byte + get() { + return when(this){ + FftViewMode.SPECTRE -> 0 + FftViewMode.X -> 1 + FftViewMode.Y -> 2 + FftViewMode.Z -> 3 + } + } + +val AccelViewMode.sendData: Byte + get() { + return when(this){ + AccelViewMode.ACCELERATION -> 0 + AccelViewMode.PEAK_ACCELERATION -> 1 + AccelViewMode.RMS -> 2 + AccelViewMode.VIBRATION -> 3 + AccelViewMode.ANGLE -> 0 + } + } + +val AccelScale.sendData: Byte + get() { + return when(this){ + AccelScale.S_2 -> 0 + AccelScale.S_4 -> 1 + AccelScale.S_8 -> 2 + AccelScale.S_16 -> 3 + } + } val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002") -val accelerometerDescriptorUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002") val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb") val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002") +val accelerometerHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002") val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb") val intervalReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002") val intervalWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002") @@ -56,11 +118,20 @@ fun UByteArray.toTemperature(): Float { return result } +fun ByteArray.get2byteUIntAt(idx: Int) = + ((this[idx + 1].toUInt() and 0xFFu) shl 8) or + (this[idx].toUInt() and 0xFFu) + @Singleton 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( @@ -69,15 +140,12 @@ class BleRepositoryImpl @Inject constructor( batteryLevel = batteryLevel ?: 0, rssi = rssi, type = type, - scanTime = timestampNanos / 1_000_000 + scanTime = timestampNanos / 1_000_000, + tx = scanRecord?.txPowerLevel ?: 0, + 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) @@ -234,6 +302,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 { @@ -250,8 +336,21 @@ class BleRepositoryImpl @Inject constructor( } else { newResult.rssi } - - ) + ), + state = Ble.BleState( + tx = when (result.scanRecord?.txPowerLevel) { + -40 -> Ble.BleState.TX.MINUS_40 + -20 -> Ble.BleState.TX.MINUS_20 + -16 -> Ble.BleState.TX.MINUS_16 + -12 -> Ble.BleState.TX.MINUS_12 + -8 -> Ble.BleState.TX.MINUS_8 + -4 -> Ble.BleState.TX.MINUS_4 + 3 -> Ble.BleState.TX.PLUS_3 + 4 -> Ble.BleState.TX.PLUS_4 + else -> Ble.BleState.TX.ZERO + } + ), + accelerometerState = tState ) ) @@ -379,10 +478,6 @@ class BleRepositoryImpl @Inject constructor( ) } - - BleInfo.Type.ACCELEROMETER -> { - TODO() - } } } @@ -419,6 +514,78 @@ class BleRepositoryImpl @Inject constructor( } + private suspend fun readAccelState( + record: ScanResult + ): Result { + + val history = readHistoryInterval(record).fold( + onFailure = { + return Result.failure(it) + }, + onSuccess = { return@fold it } + ) + + val historyParams = when(record.timerEnabled){ + true -> { + + writeCharacteristic( + device = record.device, + serviceId = serviceUUID, + characteristicId = accelerometerReadUUID, + writeData = byteArrayOf(4) + ).onFailure { + return Result.failure(BleException.UnexpectedResponse) + } + + readCharacteristic( + device = record.device, + serviceId = serviceUUID, + characteristicId = accelerometerReadUUID, + ).fold( + onSuccess = { + Log.d("history", it.joinToString { it.toString() }) + val scale = when(it[1].toInt()){ + 0 -> AccelScale.S_2 + 1 -> AccelScale.S_4 + 2 -> AccelScale.S_8 + 3 -> AccelScale.S_16 + else -> { + return Result.failure(BleException.UnexpectedResponse) + } + } + val mode = when(it[0].toInt()){ + 0 -> AccelViewMode.ACCELERATION + 1 -> AccelViewMode.PEAK_ACCELERATION + 2 -> AccelViewMode.RMS + 3 -> AccelViewMode.VIBRATION + 4 -> AccelViewMode.ANGLE + else -> { + return Result.failure(BleException.UnexpectedResponse) + } + } + Ble.Accelerometer.History.Enabled( + scale = scale, + mode = mode, + detailed = false //TODO + ) + }, + onFailure = { + return Result.failure(BleException.UnexpectedResponse) + } + ) + } + false -> Ble.Accelerometer.History.Disabled + } + + return Result.success( + Ble.Accelerometer.AccelerometerState( + saveHistory = historyParams, + historyInterval = history + ) + ) + + } + @OptIn(ExperimentalUnsignedTypes::class) private suspend fun readTemperature( record: ScanResult @@ -484,9 +651,14 @@ class BleRepositoryImpl @Inject constructor( } - override suspend fun getTemperatureHistoryBySerial( - serial: String - ): Flow>, BleException>> { + override suspend fun getAccelerometerSpectreBySerial( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency + ): Flow>, BleException>> { var gatt: BluetoothGatt? = null @@ -496,7 +668,9 @@ class BleRepositoryImpl @Inject constructor( if (checkPermission()) { - gatt = it.connectGatt(app, false, ReadHistoryCallback(app) { + gatt = it.connectGatt(app, false, ReadAccelerometerSpectreCallback( + app, accelScale, accelMode, fftAxis, fftMode, frequency + ) { CoroutineScope(Dispatchers.IO).launch { send(it) } @@ -514,6 +688,143 @@ class BleRepositoryImpl @Inject constructor( } + awaitClose { + Log.d("measure", "close") + gatt?.close() + } + + } + + } + + override suspend fun getTemperatureHistoryBySerial( + serial: String + ): Flow>, BleException>> { + + var gatt: BluetoothGatt? = null + + return callbackFlow { + + deviceCache[serial]?.device?.let { + + if (checkPermission()) { + + 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>, BleException>> { + + var gatt: BluetoothGatt? = null + + return callbackFlow { + + deviceCache[serial]?.let { result -> + + val device = result.device + + if (checkPermission()) { + + val state = readAccelState(result).fold( + onSuccess = { + + }, + onFailure = { + null + } + ) + + var scale: AccelScale? = null + var mode: AccelViewMode? = null + + writeCharacteristic( + device = device, + serviceId = serviceUUID, + characteristicId = accelerometerReadUUID, + writeData = byteArrayOf(4) + ).fold( + onSuccess = { + readCharacteristic( + device = device, + serviceId = serviceUUID, + characteristicId = accelerometerReadUUID, + ).fold( + onSuccess = { + scale = when(it[1].toInt()){ + 0 -> AccelScale.S_2 + 1 -> AccelScale.S_4 + 2 -> AccelScale.S_8 + 3 -> AccelScale.S_16 + else -> null + } + mode = when(it[0].toInt()){ + 0 -> AccelViewMode.ACCELERATION + 1 -> AccelViewMode.PEAK_ACCELERATION + 2 -> AccelViewMode.RMS + 3 -> AccelViewMode.VIBRATION + 4 -> AccelViewMode.ANGLE + else -> null + } + }, + onFailure = { null } + ) + }, + onFailure = { + null + } + ) + + if(scale != null && mode != null) { + + gatt = device.connectGatt( + app, + false, + ReadAccelerometerHistoryCallback(mode!!, scale!!, app) { + CoroutineScope(Dispatchers.IO).launch { + send(it) + } + }) + + } else { + CoroutineScope(Dispatchers.IO).launch { + send(Result.failure(BleException.UnexpectedResponse)) + } + } + + } else { + + CoroutineScope(Dispatchers.IO).launch { + send(Result.failure(BleException.PermissionDenied)) + } + + } + + } + awaitClose { gatt?.close() } @@ -594,6 +905,42 @@ class BleRepositoryImpl @Inject constructor( } + override suspend fun writeBle( + serial: String, + request: Ble.Accelerometer.WriteRequest + ): Result = suspendCancellableCoroutine { + + deviceCache[serial]?.let { scanResult -> + + if(checkPermission()) { + + var gatt: BluetoothGatt? = null + + val callback = WriteAccelerometerCallback(app, request) { result -> + + gatt?.close() + + result.onSuccess { + deviceCache.remove(serial) + resultList.remove(serial) + } + + it.resume(result) + + } + + gatt = scanResult.device.connectGatt(app, false, callback) + + } else { + + it.resume(Result.failure(BleException.PermissionDenied)) + + } + + } + + } + override suspend fun changeBlePassword( password: String, serial: String @@ -618,52 +965,51 @@ class BleRepositoryImpl @Inject constructor( return Result.success(Unit) } - override fun getAccelerometerMeasureBySerialFlow(serial: String): Flow> { + override fun getAccelerometerMeasureBySerialFlow( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency, + ): Flow> { return callbackFlow { - deviceCache[serial]?.let { + var gatt: BluetoothGatt? = null - it.device.connectGatt(app, false, object : BluetoothGattCallback() { + if(checkPermission()) { - override fun onConnectionStateChange( - gatt: BluetoothGatt, - status: Int, - newState: Int - ) { - gatt.discoverServices() - } + deviceCache[serial]?.let { - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - super.onServicesDiscovered(gatt, status) - gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let { - gatt.setCharacteristicNotification(it, true) - gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + gatt = it.device.connectGatt( + app, + false, + ReadAccelerometerCallback( + app = app, + accelScale = accelScale, + accelMode = accelMode, + fftAxis = fftAxis, + fftMode = fftMode, + frequency = frequency + ) { result -> + CoroutineScope(Dispatchers.IO).launch { + send(result) + } } - } + ) - override fun onCharacteristicChanged( - gatt: BluetoothGatt, - characteristic: BluetoothGattCharacteristic, - value: ByteArray - ) { + } - Log.d("new", value.toString()) + awaitClose { + gatt?.close() + } - CoroutineScope(Dispatchers.IO).launch { + } else { + CoroutineScope(Dispatchers.IO).launch { - send( - Result.success(((-256)..256).random().toFloat()) - ) - - } - - } - - }) - - } - awaitClose { + send(Result.failure(BleException.PermissionDenied)) + } } @@ -688,8 +1034,6 @@ class BleRepositoryImpl @Inject constructor( newState: Int ) { - Log.d("read", "onConnectionStateChange $newState $status") - if(status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED) { @@ -699,16 +1043,18 @@ class BleRepositoryImpl @Inject constructor( gatt.discoverServices() } else { - it.resume(Result.failure(BleException.PermissionDenied)) - + gatt.disconnect() } + } else { + gatt.close() } } else { it.resume(Result.failure(BleException.PermissionDenied)) + gatt.disconnect() } @@ -720,8 +1066,6 @@ class BleRepositoryImpl @Inject constructor( ) { super.onServicesDiscovered(gatt, status) - Log.d("read", "onServicesDiscovered $status") - if (status == BluetoothGatt.GATT_SUCCESS) { gatt.services?.firstOrNull { service -> @@ -736,6 +1080,7 @@ class BleRepositoryImpl @Inject constructor( } else { + gatt.disconnect() it.resume(Result.failure(BleException.PermissionDenied)) } @@ -744,10 +1089,14 @@ class BleRepositoryImpl @Inject constructor( } + gatt.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) - }else{ + } else { + + gatt.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) + } } @@ -787,8 +1136,6 @@ class BleRepositoryImpl @Inject constructor( if(status == BluetoothGatt.GATT_SUCCESS) { if (checkPermission()) { - - gatt.close() result = value it.resume(Result.success(result!!)) @@ -796,11 +1143,11 @@ class BleRepositoryImpl @Inject constructor( it.resume(Result.failure(BleException.PermissionDenied)) } - + gatt.disconnect() } else { it.resume(Result.failure(BleException.UnexpectedResponse)) - + gatt.disconnect() } } @@ -832,8 +1179,6 @@ class BleRepositoryImpl @Inject constructor( newState: Int ) { - Log.d("write", "onConnectionStateChange $status $newState") - if (status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED) { @@ -844,7 +1189,7 @@ class BleRepositoryImpl @Inject constructor( } else { - bleGatt?.close() + bleGatt?.disconnect() it.resume(Result.failure(BleException.PermissionDenied)) } @@ -852,13 +1197,12 @@ class BleRepositoryImpl @Inject constructor( } else { bleGatt?.close() - it.resume(Result.success(Unit)) } } else { - bleGatt?.close() + bleGatt?.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) } @@ -871,8 +1215,6 @@ class BleRepositoryImpl @Inject constructor( ) { super.onServicesDiscovered(gatt, status) - Log.d("write", "onServicesDiscovered $status") - if (status == BluetoothGatt.GATT_SUCCESS) { gatt.services?.firstOrNull { service -> @@ -887,7 +1229,7 @@ class BleRepositoryImpl @Inject constructor( } else { - bleGatt?.close() + bleGatt?.disconnect() it.resume(Result.failure(BleException.PermissionDenied)) } @@ -898,12 +1240,12 @@ class BleRepositoryImpl @Inject constructor( Log.d("write", "service not found") - bleGatt?.close() + bleGatt?.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) } else { - bleGatt?.close() + bleGatt?.disconnect() it.resume(Result.failure(BleException.UnexpectedResponse)) } @@ -917,17 +1259,13 @@ class BleRepositoryImpl @Inject constructor( ) { super.onCharacteristicWrite(gatt, characteristic, status) - Log.d("write", "onCharacteristicWrite $status") - if (checkPermission()) { - gatt.close() - if(status == BluetoothGatt.GATT_SUCCESS) { it.resume(Result.success(Unit)) - }else{ + } else { it.resume(Result.failure(BleException.UnexpectedResponse)) @@ -935,11 +1273,12 @@ class BleRepositoryImpl @Inject constructor( } else { - gatt.close() it.resume(Result.failure(BleException.PermissionDenied)) } + gatt.disconnect() + } } diff --git a/app/src/main/java/llc/arma/ble/data/EmailRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/EmailRepositoryImpl.kt new file mode 100644 index 0000000..fab9f76 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/EmailRepositoryImpl.kt @@ -0,0 +1,41 @@ +package llc.arma.ble.data + +import android.app.Application +import android.content.Intent +import androidx.core.content.FileProvider +import llc.arma.ble.R +import llc.arma.ble.domain.repository.EmailRepository +import llc.arma.ble.domain.repository.XlsxRepository +import llc.arma.ble.domain.usecase.MeasureData +import org.apache.poi.ss.SpreadsheetVersion +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.apache.poi.ss.util.AreaReference +import org.apache.poi.ss.util.CellReference +import org.apache.poi.util.IOUtils +import org.apache.poi.xssf.usermodel.XSSFSheet +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.UUID +import javax.inject.Inject + +class EmailRepositoryImpl @Inject constructor( + private val application: Application +) : EmailRepository { + + override fun sendFile(file: File) { + val uri = FileProvider.getUriForFile(application, "llc.arma.ble.fileprovider", file) + + val sendIntent = Intent(Intent.ACTION_SEND) + sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + sendIntent.type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + sendIntent.putExtra(Intent.EXTRA_SUBJECT, "Measure history") + sendIntent.putExtra(Intent.EXTRA_STREAM, uri) + application.startActivity( + Intent.createChooser(sendIntent, "Send email...").apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/ReadAccelerometerCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerCallback.kt new file mode 100644 index 0000000..6b2420b --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerCallback.kt @@ -0,0 +1,297 @@ +package llc.arma.ble.data + +import android.Manifest +import android.app.Application +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import llc.arma.ble.domain.Result +import llc.arma.ble.domain.common.BleException +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode +import llc.arma.ble.domain.usecase.MeasureData +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode +import java.util.UUID +import kotlin.math.PI +import kotlin.math.atan +import kotlin.math.pow +import kotlin.math.sqrt + + +class ReadAccelerometerCallback( + private val app: Application, + private val accelScale: AccelScale, + private val accelMode: AccelViewMode, + private val fftAxis: FftAxis, + private val fftMode: FftViewMode, + private val frequency: FftFrequency, + private val onResult: (Result) -> Unit +) : BluetoothGattCallback() { + + override fun onConnectionStateChange( + gatt: BluetoothGatt, + status: Int, + newState: Int + ) { + super.onConnectionStateChange(gatt, status, newState) + + if(status == BluetoothGatt.GATT_SUCCESS){ + + if(newState == BluetoothGatt.STATE_CONNECTED){ + + if (checkPermission()) { + gatt.discoverServices() + + } else { + onResult(Result.failure(BleException.UnexpectedResponse)) + gatt.close() + } + + } + + } else { + + onResult(Result.failure(BleException.UnexpectedResponse)) + gatt.close() + + } + + } + + override fun onServicesDiscovered( + gatt: BluetoothGatt, + status: Int + ) { + super.onServicesDiscovered(gatt, status) + + Log.d("accel", "onServicesDiscovered") + + if(status == BluetoothGatt.GATT_SUCCESS){ + + gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let { + + if (checkPermission()) { + + val payload = byteArrayOf( + 4, + accelMode.sendData, + accelScale.sendData, + fftMode.sendData, + fftAxis.sendData, + frequency.sendData, + 1 + ) + + gatt.writeCharacteristic(it, payload) + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + gatt.close() + + } + + } + + } + + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + + Log.d("accel", "request written") + + if(status == BluetoothGatt.GATT_SUCCESS){ + + gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let { + + if (checkPermission()) { + + gatt.setCharacteristicNotification(it, true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + gatt.close() + + } + + } + + } + + } + + @Deprecated("Deprecated in Java") + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicChanged(gatt, characteristic) + onCommonCharacteristicRead(characteristic.value) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + onCommonCharacteristicRead(value) + } + + private fun onCommonCharacteristicRead( + value: ByteArray, + ){ + + + + val result = if(accelMode == AccelViewMode.VIBRATION){ + + MeasureData.Vibration((value.get2byteShortAt().toFloat() * accelScale.k) / Short.MAX_VALUE) + + } else { + + val data = value.toList().chunked(2).map { + it.toByteArray().get2byteShortAt() + } + + Log.d("accel", "x: ${data[0]} y: ${data[1]} z: ${data[2]} bytes: ${value.joinToString { it.toString() }}") + + val x: Float + val y: Float + val z: Float + + if (accelMode == AccelViewMode.ANGLE) { + + x = calculateZAngle(data[2].toFloat(), data[1].toFloat()) * 180f / Math.PI.toFloat() + y = calculateZAngle(data[2].toFloat(), data[0].toFloat()) * 180f / Math.PI.toFloat() + z = calculateZAngle(data[0].toFloat(), data[1].toFloat()) * 180f / Math.PI.toFloat() + + } else { + + x = (data[0].toFloat() * accelScale.k) / Short.MAX_VALUE + y = (data[1].toFloat() * accelScale.k) / Short.MAX_VALUE + z = (data[2].toFloat() * accelScale.k) / Short.MAX_VALUE + + } + + val state = MeasureData.Accelerate( + x = x, + y = y, + z = z + ) + + state + + } + + onResult(Result.success(result)) + + } + + private fun checkPermission(): Boolean { + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) == + PackageManager.PERMISSION_GRANTED + } else { + return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + } + } + + private fun BluetoothGatt.writeCharacteristic( + characteristic: BluetoothGattCharacteristic, + data: ByteArray + ): Result{ + + 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) + } + + } + +} + +fun calculateAngle( + targetAxis: Float, + firstAxis: Float, + secondAxis: Float +): Float { + + return atan(targetAxis.div(sqrt(firstAxis.pow(2) + secondAxis.pow(2)))) + +} + +public fun calculateZAngle( + x: Float, + y: Float +): Float { + + var x = x + + if(x == 0f && y == 0f){ + x = 0.0001f + } + + if(x > 0){ + return atan(y/x) + } + + if(x < 0 && y >= 0){ + return atan(y/x) + PI.toFloat() + } + + if(x < 0 && y < 0){ + return atan(y/x) - PI.toFloat() + } + + if(x == 0f && y > 0){ + return PI.toFloat() / 2f + } + + if(x == 0f && y < 0){ + return -PI.toFloat() / 2f + } + + return 0f + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/ReadAccelerometerHistoryCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerHistoryCallback.kt new file mode 100644 index 0000000..dca14e3 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerHistoryCallback.kt @@ -0,0 +1,410 @@ +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 +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode + +class ReadAccelerometerHistoryCallback( + private val mode: AccelViewMode, + private val scale: AccelScale, + private val app: Application, + private val onResult: (Result>, 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 { + Log.d("history", scale.name) + 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 = mutableListOf() + + private val result = 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 { + + Log.d("expected data size", value.get2byteUIntAt(0).toString()) + + 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()){ + + result.add(value.toUByteArray().toList()) + + bleMeasureInterval = value.get4byteUIntAt(4).toLong() + bleLastMeasureTime = value.get4byteUIntAt(8).toLong() + bleRealTime = value.get4byteUIntAt(12).toLong() + + lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) * 1_000) + + val nextPackageDataCount = value.get2byteUIntAt(2) + val temperatureDataArray = value.toUByteArray().asList().subList(16, value.size) + + + + resultTemperaturePackage.addAll( + temperatureDataArray.chunked(2).map { + it.toUByteArray().toByteArray().get2byteShortAt().toFloat() + }.toMutableList() + ) + + Log.d("received data size", (temperatureDataArray.chunked(2).size).toString()) + Log.d("next data size", nextPackageDataCount.toString()) + + 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( + when(mode){ + AccelViewMode.ACCELERATION, + AccelViewMode.PEAK_ACCELERATION, + AccelViewMode.RMS -> { + resultTemperaturePackage.chunked(3).withIndex().map { + Ble.Accelerometer.HistoryPoint.Angle( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + x = (it.value[0] * scale.k) / Short.MAX_VALUE, + y = (it.value[1] * scale.k) / Short.MAX_VALUE, + z = (it.value[2] * scale.k) / Short.MAX_VALUE + ) + } + } + AccelViewMode.ANGLE -> { + resultTemperaturePackage.chunked(3).withIndex().map { + Ble.Accelerometer.HistoryPoint.Angle( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + x = calculateZAngle(it.value[2], it.value[1]) * 180f / Math.PI.toFloat(), + y = calculateZAngle(it.value[2], it.value[0]) * 180f / Math.PI.toFloat(), + z = calculateZAngle(it.value[0], it.value[1]) * 180f / Math.PI.toFloat() + ) + } + } + AccelViewMode.VIBRATION -> { + resultTemperaturePackage.withIndex().map { + Ble.Accelerometer.HistoryPoint.Vibration( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + value = (it.value * scale.k) / Short.MAX_VALUE + ) + } + } + } + ) + ) + ) + gatt.close() + } + + } else { + + if (value[0] == 251.toByte()) { + + result.add(value.toUByteArray().toList()) + + 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() + } + ) + + Log.d("received data size", (temperatureDataArray.chunked(2).size).toString()) + Log.d("next data size", nextPackageDataCount.toString()) + + 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( + when(mode){ + AccelViewMode.ACCELERATION, + AccelViewMode.PEAK_ACCELERATION, + AccelViewMode.RMS -> { + resultTemperaturePackage.chunked(3).withIndex().map { + Ble.Accelerometer.HistoryPoint.Angle( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + x = (it.value[0] * scale.k) / Short.MAX_VALUE, + y = (it.value[1] * scale.k) / Short.MAX_VALUE, + z = (it.value[2] * scale.k) / Short.MAX_VALUE + ) + } + } + AccelViewMode.ANGLE -> { + resultTemperaturePackage.chunked(3).withIndex().map { + Ble.Accelerometer.HistoryPoint.Angle( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + x = calculateZAngle(it.value[2], it.value[1]) * 180f / Math.PI.toFloat(), + y = calculateZAngle(it.value[2], it.value[0]) * 180f / Math.PI.toFloat(), + z = calculateZAngle(it.value[0], it.value[1]) * 180f / Math.PI.toFloat() + ) + } + } + AccelViewMode.VIBRATION -> { + resultTemperaturePackage.withIndex().map { + Ble.Accelerometer.HistoryPoint.Vibration( + date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), + value = (it.value * scale.k) / Short.MAX_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{ + + 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) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/ReadAccelerometerSpectreCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerSpectreCallback.kt new file mode 100644 index 0000000..573ec2a --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/ReadAccelerometerSpectreCallback.kt @@ -0,0 +1,464 @@ +package llc.arma.ble.data + +import android.Manifest +import android.app.Application +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import llc.arma.ble.domain.Result +import llc.arma.ble.domain.common.BleException +import llc.arma.ble.domain.common.ProgressState +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.usecase.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 java.nio.ByteBuffer +import java.nio.ByteOrder.LITTLE_ENDIAN +import java.util.UUID + +fun ByteArray.get2byteShortAt(): Int { + val shorts = ShortArray(1) + ByteBuffer.wrap(this).order(LITTLE_ENDIAN).asShortBuffer()[shorts] + return shorts[0].toInt() +} + +class ReadAccelerometerSpectreCallback( + private val app: Application, + private val accelScale: AccelScale, + private val accelMode: AccelViewMode, + private val fftAxis: FftAxis, + private val fftMode: FftViewMode, + private val frequency: FftFrequency, + private val onResult: (Result>, 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){ + enableNotifications(gatt) + } + + } + + private fun enableNotifications( + gatt: BluetoothGatt + ){ + + gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let { + + if (checkPermission()) { + + gatt.setCharacteristicNotification(it, true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } + + Log.d("spectre", "enable notification") + + onResult(Result.success(ProgressState.Indeterminate)) + resultAccelerometerPackage.clear() + + } else { + onResult(Result.failure(BleException.PermissionDenied)) + gatt.close() + } + + return + + } + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } + + private var initialValue: Long? = null + private var frequencyInterval: Long? = null + + private val resultAccelerometerPackage: MutableList = mutableListOf() + + private var expectedDataSize: Int? = null + + @Deprecated("Deprecated in Java") + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicRead(gatt, characteristic, status) + onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status) + } + } + + @Deprecated("Deprecated in Java") + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic + ) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + super.onCharacteristicChanged(gatt, characteristic) + onCommonCharacteristicChanged(gatt, characteristic, characteristic.value) + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray + ) { + super.onCharacteristicChanged(gatt, characteristic, value) + onCommonCharacteristicChanged(gatt, characteristic, value) + } + + private fun onCommonCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + ){ + + if(characteristic.uuid == accelerometerReadUUID) { + + readProperty = Property.DATA_SIZE + gatt.getService(serviceUUID).getCharacteristic(accelerometerHistoryReadUUID)?.let { + gatt.writeCharacteristic(it, byteArrayOf(2)) + } + + } + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + onCommonCharacteristicRead(gatt, characteristic, value, status) + } + + 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()){ + + initialValue = value.get4byteUIntAt(8).toLong() + frequencyInterval = value.get4byteUIntAt(4).toLong() + + val accelerometerDataArray = value.asList().subList(16, value.size) + + resultAccelerometerPackage.addAll( + accelerometerDataArray.chunked(2).map { + it.toByteArray().get2byteShortAt().toFloat() + }.toMutableList() + ) + + val nextPackageDataCount = value.get2byteUIntAt(2) + expectedDataSize = nextPackageDataCount.toInt() + resultAccelerometerPackage.size + + onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat()))) + onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat()))) + + if(nextPackageDataCount != 0.toUInt()){ + + if (checkPermission()) { + + gatt.writeCharacteristic(characteristic, byteArrayOf(5)) + gatt.readCharacteristic(characteristic) + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + gatt.close() + + } + + } else { + + onResult( + Result.success( + ProgressState.Finished( + resultAccelerometerPackage.withIndex().map { + Ble.Accelerometer.MeasurePoint( + frequency = frequencyInterval!! * it.index + initialValue!!, + value = (it.value * accelScale.k) / Short.MAX_VALUE + ) + } + ) + ) + ) + + start(gatt) + + } + + } else { + + if (value[0] == 251.toByte()) { + + val nextPackageDataCount = value.get2byteUIntAt(2) + val temperatureDataArray = value.toList().subList(4, value.size) + + resultAccelerometerPackage.addAll( + temperatureDataArray.chunked(2).map { + it.toByteArray().get2byteShortAt().toFloat() + } + ) + + onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat()))) + + if (nextPackageDataCount != 0.toUInt()) { + + val writeData = byteArrayOf(5) + + gatt.writeCharacteristic(characteristic, writeData) + gatt.readCharacteristic(characteristic) + + } else { + + onResult( + Result.success( + ProgressState.Finished( + resultAccelerometerPackage.withIndex().map { + Ble.Accelerometer.MeasurePoint( + frequency = frequencyInterval!! * it.index + initialValue!!, + value = (it.value * accelScale.k) / Short.MAX_VALUE + ) + } + ) + ) + ) + + start(gatt) + + } + } 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(readProperty !== null) { + + 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() + } + } + + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int + ) { + super.onDescriptorWrite(gatt, descriptor, status) + start(gatt) + } + + private fun start( + gatt: BluetoothGatt, + ){ + + gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let { + + if (checkPermission()) { + + val payload = byteArrayOf( + 4, + accelMode.sendData, + accelScale.sendData, + fftMode.sendData, + fftAxis.sendData, + frequency.sendData, + 2 + ) + readProperty = null + gatt.writeCharacteristic(it, payload) + + onResult(Result.success(ProgressState.Indeterminate)) + resultAccelerometerPackage.clear() + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + gatt.close() + + } + + return + + } + + } + + private fun checkPermission(): Boolean { + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) == + PackageManager.PERMISSION_GRANTED + } else { + return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + } + } + + private fun BluetoothGatt.writeCharacteristic( + characteristic: BluetoothGattCharacteristic, + data: ByteArray + ): Result{ + + 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) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt b/app/src/main/java/llc/arma/ble/data/ReadTemperatureHistoryCallback.kt similarity index 98% rename from app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt rename to app/src/main/java/llc/arma/ble/data/ReadTemperatureHistoryCallback.kt index a8fcb0a..8c7ef9d 100644 --- a/app/src/main/java/llc/arma/ble/data/ReadHistoryCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/ReadTemperatureHistoryCallback.kt @@ -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>, 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 @@ -133,7 +132,6 @@ class ReadHistoryCallback( value: ByteArray, status: Int ){ - Log.d("read", value[0].toString()) if(status == BluetoothGatt.GATT_SUCCESS){ when(readProperty){ diff --git a/app/src/main/java/llc/arma/ble/data/WriteAccelerometerCallback.kt b/app/src/main/java/llc/arma/ble/data/WriteAccelerometerCallback.kt new file mode 100644 index 0000000..83253e0 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/WriteAccelerometerCallback.kt @@ -0,0 +1,245 @@ +package llc.arma.ble.data + +import android.Manifest +import android.app.Application +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothProfile +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import llc.arma.ble.domain.Result +import llc.arma.ble.domain.common.BleException +import llc.arma.ble.domain.model.Ble +import java.util.UUID + +class WriteAccelerometerCallback( + private val app: Application, + private var request: Ble.Accelerometer.WriteRequest, + private val onResult: (Result) -> Unit +) : BluetoothGattCallback() { + + private var flashed = false + + override fun onConnectionStateChange( + gatt: BluetoothGatt, + status: Int, + newState: Int + ) { + super.onConnectionStateChange(gatt, status, newState) + + if(checkPermission()) { + + if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { + + gatt.discoverServices() + + } else { + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + + } + + } + + override fun onServicesDiscovered( + gatt: BluetoothGatt, + status: Int + ) { + super.onServicesDiscovered(gatt, status) + onCycle(gatt, status) + + } + + private fun onCycle( + gatt: BluetoothGatt, + status: Int + ){ + + Log.d("write", "${request.tx != null} ${request.saveHistory != null} ${request.historyInterval != null}") + + if(request.tx != null || request.saveHistory != null || request.historyInterval != null) { + + fun UInt.to4ByteArrayInLittleEndian(): ByteArray = + (3 downTo 0).map { + (this shr (it * Byte.SIZE_BITS)).toByte() + }.toByteArray() + + var uuid: Triple? = null + + uuid = request.historyInterval?.let { + + Triple( + intervalWriteUUID, + mutableListOf(3).apply { + addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()) + }.toByteArray(), + request.copy( + historyInterval = null + ) + ) + } + + uuid = request.saveHistory?.let { + + Triple( + saveEnabledWriteUUID, + mutableListOf(4).apply { + add(if (it is Ble.Accelerometer.History.Enabled) 1 else 0) + if(it is Ble.Accelerometer.History.Enabled) { + add(it.mode.sendData) + add(it.scale.sendData) + } + }.toByteArray(), + request.copy( + saveHistory = null + ) + ) + } ?: uuid + + uuid = request.tx?.let { + + Triple( + txWriteUUID, + byteArrayOf( + when (it) { + Ble.BleState.TX.MINUS_40 -> -40 + Ble.BleState.TX.MINUS_20 -> -20 + Ble.BleState.TX.MINUS_16 -> -16 + Ble.BleState.TX.MINUS_12 -> -12 + Ble.BleState.TX.MINUS_8 -> -8 + Ble.BleState.TX.MINUS_4 -> -4 + Ble.BleState.TX.ZERO -> 0 + Ble.BleState.TX.PLUS_3 -> 3 + Ble.BleState.TX.PLUS_4 -> 4 + } + ), + request.copy( + tx = null + ) + ) + + } ?: uuid + + uuid?.let { uuid -> + + gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull { + it.uuid == uuid.first + }?.let { + + gatt.writeCharacteristic(it, uuid.second) + + request = uuid.third + + return + + } + + } + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } else { + + if(flashed.not()){ + + flashed = true + + gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull { + it.uuid == flashWriteUUID + }?.let { + + gatt.writeCharacteristic(it, byteArrayOf(9)) + + return + + } + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } else { + + onResult(Result.success(Unit)) + + } + + } + + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + + super.onCharacteristicWrite(gatt, characteristic, status) + + if(checkPermission()) { + + if(status == BluetoothGatt.GATT_SUCCESS || flashed) { + + onCycle(gatt, status) + + } else { + + onResult(Result.failure(BleException.UnexpectedResponse)) + + } + + } else { + + onResult(Result.failure(BleException.PermissionDenied)) + + } + + } + + fun BluetoothGatt.writeCharacteristic( + characteristic: BluetoothGattCharacteristic, + data: ByteArray + ): Result { + + return if(checkPermission()){ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + }else{ + + characteristic.writeType + characteristic.value = data + writeCharacteristic(characteristic) + } + + Result.success(Unit) + + } else { + Result.failure(BleException.PermissionDenied) + } + + } + + fun checkPermission(): Boolean { + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) == + PackageManager.PERMISSION_GRANTED + } else { + return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/WriteBeaconCallback.kt b/app/src/main/java/llc/arma/ble/data/WriteBeaconCallback.kt index 91a977a..cae6bcd 100644 --- a/app/src/main/java/llc/arma/ble/data/WriteBeaconCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/WriteBeaconCallback.kt @@ -30,8 +30,6 @@ class WriteBeaconCallback( ) { super.onConnectionStateChange(gatt, status, newState) - Log.d("beacon", "onConnectionStateChange $status $newState") - if(checkPermission()) { if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { @@ -56,7 +54,6 @@ class WriteBeaconCallback( gatt: BluetoothGatt, status: Int ) { - Log.d("beacon", "onServicesDiscovered $status") super.onServicesDiscovered(gatt, status) onCycle(gatt, status) diff --git a/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt index d12574b..a9fe747 100644 --- a/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt +++ b/app/src/main/java/llc/arma/ble/data/WriteThermometerCallback.kt @@ -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) @@ -78,43 +75,37 @@ class WriteThermometerCallback( (this shr (it * Byte.SIZE_BITS)).toByte() }.toByteArray() - var uuid: Pair? = null + var uuid: Triple? = null uuid = request.historyInterval?.let { - this.request = request.copy( - historyInterval = null - ) - - Pair( + Triple( intervalWriteUUID, mutableListOf(3).apply { addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()) - }.toByteArray() + }.toByteArray(), + request.copy( + historyInterval = null + ) ) } uuid = request.saveHistory?.let { - this.request = request.copy( - saveHistory = null - ) - - Pair( + Triple( saveEnabledWriteUUID, mutableListOf(4).apply { add(if (it) 1 else 0) - }.toByteArray() + }.toByteArray(), + request.copy( + saveHistory = null + ) ) } ?: uuid uuid = request.tx?.let { - this.request = request.copy( - tx = null - ) - - Pair( + Triple( txWriteUUID, byteArrayOf( when (it) { @@ -128,6 +119,9 @@ class WriteThermometerCallback( Ble.BleState.TX.PLUS_3 -> 3 Ble.BleState.TX.PLUS_4 -> 4 } + ), + request.copy( + tx = null ) ) @@ -140,7 +134,7 @@ class WriteThermometerCallback( }?.let { gatt.writeCharacteristic(it, uuid.second) - + request = uuid.third return } @@ -183,8 +177,6 @@ class WriteThermometerCallback( status: Int ) { - Log.d("th", "onCharacteristicWrite $status") - super.onCharacteristicWrite(gatt, characteristic, status) if(checkPermission()) { diff --git a/app/src/main/java/llc/arma/ble/data/XlsxRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/XlsxRepositoryImpl.kt new file mode 100644 index 0000000..71f0a03 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/data/XlsxRepositoryImpl.kt @@ -0,0 +1,99 @@ +package llc.arma.ble.data + +import android.R.attr.src +import android.app.Application +import android.icu.text.SimpleDateFormat +import android.os.Environment +import android.os.FileUtils +import llc.arma.ble.R +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.repository.XlsxRepository +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.apache.poi.util.IOUtils +import org.apache.poi.xssf.usermodel.XSSFSheet +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.Date +import java.util.UUID +import javax.inject.Inject + + +class XlsxRepositoryImpl @Inject constructor( + private val application: Application +) : XlsxRepository { + + override fun exportToXls(data: List): File { + + val formatter = SimpleDateFormat("dd.MM.yyyy HH:mm") + + val mailFile = File(application.filesDir, "${UUID.randomUUID()}.xlsx") + mailFile.createNewFile() + + when(data.firstOrNull()){ + is Ble.Accelerometer.HistoryPoint.Vibration -> { + IOUtils.copy(application.resources.openRawResource(R.raw.measure_single_axis), FileOutputStream(mailFile)) + } + else -> { + IOUtils.copy(application.resources.openRawResource(R.raw.measure_multiple_axis), FileOutputStream(mailFile)) + } + } + + val fileIn = FileInputStream(mailFile) + val workbook = WorkbookFactory.create(fileIn) + val worksheet = workbook.getSheetAt(0) as XSSFSheet + + data.withIndex().forEach { + + val row = worksheet.createRow(it.index + 1) + val dateX = row.createCell(0) + val dateY = row.createCell(2) + val dateZ = row.createCell(4) + val x = row.createCell(1) + val y = row.createCell(3) + val z = row.createCell(5) + + when(val value = it.value){ + is Ble.Accelerometer.HistoryPoint.Angle -> { + dateX.setCellValue(formatter.format(Date(value.date))) + dateY.setCellValue(formatter.format(Date(value.date))) + dateZ.setCellValue(formatter.format(Date(value.date))) + x.setCellValue(value.x.toDouble()) + y.setCellValue(value.y.toDouble()) + z.setCellValue(value.z.toDouble()) + } + is Ble.Accelerometer.HistoryPoint.Vibration -> { + dateX.setCellValue(formatter.format(Date(value.date))) + x.setCellValue(value.value.toDouble()) + } + + is Ble.Accelerometer.HistoryPoint.Accelerate -> { + dateX.setCellValue(formatter.format(Date(value.date))) + dateY.setCellValue(formatter.format(Date(value.date))) + dateZ.setCellValue(formatter.format(Date(value.date))) + x.setCellValue(value.x.toDouble()) + y.setCellValue(value.y.toDouble()) + z.setCellValue(value.z.toDouble()) + } + + } + } + + fileIn.close() + val saveFos = FileOutputStream(mailFile) + workbook.write(saveFos) + saveFos.close() + + val sharedFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "${UUID.randomUUID()}.xlsx") + sharedFile.createNewFile() + + val sharedSaveFos = FileOutputStream(sharedFile) + workbook.write(sharedSaveFos) + sharedSaveFos.close() + + workbook.close() + + return mailFile + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt index e09f848..4c82b99 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt @@ -1,12 +1,68 @@ package llc.arma.ble.domain.model +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode + sealed class Ble( val info: BleInfo ) { class Accelerometer( - info: BleInfo - ): Ble(info){ + info: BleInfo, + val state: BleState, + val accelerometerState: AccelerometerState + ): Ble(info) { + + sealed class History { + + data class Enabled( + val scale: AccelScale, + val mode: AccelViewMode, + val detailed: Boolean + ) : History() + + object Disabled : History() + + } + + data class WriteRequest( + val tx: BleState.TX?, + val saveHistory: History?, + val historyInterval: Long? + ) + + sealed class HistoryPoint { + + class Vibration ( + val date: Long, + val value: Float + ) : HistoryPoint() + + class Accelerate ( + val date: Long, + val x: Float, + val y: Float, + val z: Float + ) : HistoryPoint() + + class Angle ( + val date: Long, + val x: Float, + val y: Float, + val z: Float + ) : HistoryPoint() + + } + + class MeasurePoint ( + val frequency: Long, + val value: Float + ) + + data class AccelerometerState( + val saveHistory: History, + val historyInterval: Long + ) } diff --git a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt index 4043f33..c3e5166 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt @@ -6,7 +6,9 @@ data class BleInfo( val batteryLevel: Int, val rssi: Int?, val type: Type, - val scanTime: Long + val scanTime: Long, + val tx: Int, + val recordEnabled: Boolean ){ enum class Type { diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt index 0be5155..cdac918 100644 --- a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt +++ b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt @@ -7,7 +7,12 @@ import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.ConnectedBleInfo -import llc.arma.ble.domain.usecase.GetBleBySerial +import llc.arma.ble.domain.usecase.AccelScale +import llc.arma.ble.domain.usecase.AccelViewMode +import llc.arma.ble.domain.usecase.MeasureData +import llc.arma.ble.domain.usecase.FftAxis +import llc.arma.ble.domain.usecase.FftFrequency +import llc.arma.ble.domain.usecase.FftViewMode interface BleRepository { @@ -23,7 +28,28 @@ interface BleRepository { suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result + suspend fun writeBle(serial: String, request: Ble.Accelerometer.WriteRequest): Result + suspend fun changeBlePassword(password: String, serial: String): Result - fun getAccelerometerMeasureBySerialFlow(serial: String): Flow> + + fun getAccelerometerMeasureBySerialFlow( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency + ): Flow> + + suspend fun getAccelerometerSpectreBySerial( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency + ): Flow>, BleException>> + + suspend fun getAccelerometerHistoryBySerial(serial: String): Flow>, BleException>> } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/repository/EmailRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/EmailRepository.kt new file mode 100644 index 0000000..fbb5170 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/repository/EmailRepository.kt @@ -0,0 +1,9 @@ +package llc.arma.ble.domain.repository + +import java.io.File + +interface EmailRepository { + + fun sendFile(file: File) + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/repository/XlsxRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/XlsxRepository.kt new file mode 100644 index 0000000..3d308b5 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/repository/XlsxRepository.kt @@ -0,0 +1,11 @@ +package llc.arma.ble.domain.repository + +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.usecase.MeasureData +import java.io.File + +interface XlsxRepository { + + fun exportToXls(data: List): File + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/ExportToXlsx.kt b/app/src/main/java/llc/arma/ble/domain/usecase/ExportToXlsx.kt new file mode 100644 index 0000000..1755a0d --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/usecase/ExportToXlsx.kt @@ -0,0 +1,34 @@ +package llc.arma.ble.domain.usecase + +import android.app.Application +import llc.arma.ble.R +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.repository.EmailRepository +import llc.arma.ble.domain.repository.XlsxRepository +import org.apache.poi.ss.SpreadsheetVersion +import org.apache.poi.ss.usermodel.WorkbookFactory +import org.apache.poi.ss.util.AreaReference +import org.apache.poi.ss.util.CellReference +import org.apache.poi.util.IOUtils +import org.apache.poi.xssf.usermodel.XSSFCell +import org.apache.poi.xssf.usermodel.XSSFSheet +import org.apache.poi.xssf.usermodel.XSSFTable +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.UUID +import javax.inject.Inject + +class ExportToXlsx @Inject constructor( + private val xlsxRepository: XlsxRepository, + private val emailRepository: EmailRepository +) { + + operator fun invoke(data: List){ + + val file = xlsxRepository.exportToXls(data) + emailRepository.sendFile(file) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerHistoryBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerHistoryBySerial.kt new file mode 100644 index 0000000..a3b0f56 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerHistoryBySerial.kt @@ -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>, BleException>> { + + return bleRepository.getAccelerometerHistoryBySerial(serial) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerMeasureBySerialFlow.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerMeasureBySerialFlow.kt index 6a5bbce..814371c 100644 --- a/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerMeasureBySerialFlow.kt +++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerMeasureBySerialFlow.kt @@ -3,8 +3,6 @@ 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 @@ -12,10 +10,54 @@ class GetAccelerometerMeasureBySerialFlow @Inject constructor( private val bleRepository: BleRepository ) { - operator fun invoke(serial: String): Flow> { + operator fun invoke( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency + ): Flow> { - return bleRepository.getAccelerometerMeasureBySerialFlow(serial) + return bleRepository.getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency) } -} \ No newline at end of file +} + +enum class AccelViewMode { + ACCELERATION, PEAK_ACCELERATION, RMS, VIBRATION, ANGLE +} + +enum class AccelScale(val k: Int) { + S_2(2_000), S_4(4_000), S_8(8_000), S_16(16_000) +} + +sealed class MeasureData { + + data class Angle( + val xAngle: Float, + val yAngle: Float, + val zAngle: Float, + val xAccelerate: Float, + val yAccelerate: Float, + val zAccelerate: Float, + ) : MeasureData() + + data class Accelerate( + val x: Float, + val y: Float, + val z: Float, + ) : MeasureData() + + data class Vibration( + val value: Float + ) : MeasureData() + +} + +data class Accelerate( + val x: Float, + val y: Float, + val z: Float, +) \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerSpectreBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerSpectreBySerial.kt new file mode 100644 index 0000000..5190ba2 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetAccelerometerSpectreBySerial.kt @@ -0,0 +1,40 @@ +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 GetAccelerometerSpectreBySerial @Inject constructor( + private val bleRepository: BleRepository +) { + + suspend operator fun invoke( + serial: String, + accelScale: AccelScale, + accelMode: AccelViewMode, + fftAxis: FftAxis, + fftMode: FftViewMode, + frequency: FftFrequency + ): Flow>, BleException>> { + + return bleRepository.getAccelerometerSpectreBySerial(serial, accelScale, accelMode, fftAxis, fftMode, frequency) + + } + +} + +enum class FftFrequency { + OFF, F_1, F_10, F_25, F_50, F_100, F_200, F_400, F_1620, F_1344 +} + +enum class FftViewMode { + SPECTRE, X, Y, Z +} + +enum class FftAxis { + AUTO, X, Y, Z +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt index 1bab00b..1732211 100644 --- a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt +++ b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt @@ -24,4 +24,11 @@ class WriteBle @Inject constructor( return bleRepository.writeBle(serial, request) } + suspend operator fun invoke( + serial: String, + request: Ble.Accelerometer.WriteRequest + ): llc.arma.ble.domain.Result{ + return bleRepository.writeBle(serial, request) + } + } \ No newline at end of file diff --git a/app/src/main/res/raw/measure_multiple_axis.xlsx b/app/src/main/res/raw/measure_multiple_axis.xlsx new file mode 100644 index 0000000..198220a Binary files /dev/null and b/app/src/main/res/raw/measure_multiple_axis.xlsx differ diff --git a/app/src/main/res/raw/measure_single_axis.xlsx b/app/src/main/res/raw/measure_single_axis.xlsx new file mode 100644 index 0000000..370a150 Binary files /dev/null and b/app/src/main/res/raw/measure_single_axis.xlsx differ diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..ef021c3 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 40dc16d..dd43d6b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.0.2' apply false - id 'com.android.library' version '8.0.2' apply false + id 'com.android.application' version '8.1.1' apply false + id 'com.android.library' version '8.1.1' apply false id 'org.jetbrains.kotlin.android' version '1.7.0' apply false } \ No newline at end of file