diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index e1d52e3..494db10 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -13,6 +13,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 7b3006b..e802310 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -11,9 +11,11 @@
+
+
+
-
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 910c7a2..3071538 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,15 +3,19 @@
+
+
+
+
@@ -27,21 +31,26 @@
+
+
+
+
+
@@ -52,8 +61,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 8d81632..5263150 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,12 @@
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b2c751a..7c748e9 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,10 @@
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 7b6d7ce..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,117 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
- id("org.jetbrains.kotlin.plugin.serialization")
- id 'kotlin-kapt'
- id 'dagger.hilt.android.plugin'
- id("kotlin-parcelize")
-
- id("androidx.room")
-}
-
-
-
-android {
- namespace 'llc.arma.ble'
- compileSdk 34
-
- defaultConfig {
- applicationId "llc.arma.ble"
- minSdk 26
- targetSdk 34
- versionCode 50
- versionName "1.4.24"
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary true
- }
- }
-
- buildTypes {
- debug {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_17
- targetCompatibility JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = '17'
- }
- buildFeatures {
- compose true
- }
- composeOptions {
- kotlinCompilerExtensionVersion '1.5.9'
- }
- packagingOptions {
- resources {
- excludes += '/META-INF/{AL2.0,LGPL2.1}'
- }
- }
-
- applicationVariants.configureEach { variant ->
- variant.outputs.configureEach {
- outputFileName = "Arma BLE v${defaultConfig.versionName}.apk"
- }
- }
-
- room {
- schemaDirectory("$projectDir/schemas")
- }
-
-}
-
-dependencies {
-
- implementation 'androidx.core:core-ktx:1.13.1'
- implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
- implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha01'
- implementation 'androidx.activity:activity-compose:1.7.2'
- implementation "androidx.compose.ui:ui:1.5.0-beta01"
- implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-beta01"
- implementation 'androidx.compose.material3:material3:1.2.0-alpha02'
- implementation 'androidx.compose.material:material:1.5.0-beta01'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
- androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0-beta01"
- debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-beta01"
- debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.0-beta01"
-
- implementation "androidx.compose.material:material-icons-extended:1.5.0-beta01"
-
- implementation 'androidx.core:core-splashscreen:1.0.1'
- implementation 'androidx.navigation:navigation-compose:2.5.3'
-
- implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
- implementation('com.google.dagger:hilt-android:2.46')
- kapt('com.google.dagger:hilt-android-compiler:2.46')
- kapt("androidx.hilt:hilt-compiler:1.2.0")
-
- implementation 'no.nordicsemi.android.kotlin.ble:scanner:1.0.14'
- implementation 'no.nordicsemi.android.kotlin.ble:client:1.0.14'
-
- implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta"
-
- 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 "androidx.room:room-runtime:2.6.1"
- kapt("androidx.room:room-compiler:2.6.1")
- implementation("androidx.room:room-ktx:2.6.1")
-
- implementation("androidx.datastore:datastore-preferences:1.1.1")
- implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
-
- implementation files('libs/poishadow-all.jar')
-
-}
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..a47cc5f
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,132 @@
+import com.android.build.gradle.internal.api.BaseVariantOutputImpl
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.room)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+
+ namespace = "llc.arma.ble"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "llc.arma.ble"
+ minSdk = 26
+ targetSdk = 35
+ versionCode = 50
+ versionName = "1.4.24"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ /*debug {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }*/
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ /*packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }*/
+
+ applicationVariants.all {
+ outputs.all {
+ (this as BaseVariantOutputImpl).outputFileName = "Arma BLE v${defaultConfig.versionName}.apk"
+ }
+ }
+
+ room {
+ schemaDirectory("$projectDir/schemas")
+ }
+
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.ui)
+ implementation(libs.ui.tooling.preview)
+ implementation(libs.material3)
+ implementation(libs.androidx.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(libs.ui.test.junit4)
+ debugImplementation(libs.ui.tooling)
+ debugImplementation(libs.ui.test.manifest)
+
+ implementation(libs.androidx.material.icons.extended)
+
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.navigation.compose)
+
+ implementation("io.github.raamcosta.compose-destinations:core:2.1.1")
+ ksp("io.github.raamcosta.compose-destinations:ksp:2.1.1")
+ implementation("io.github.raamcosta.compose-destinations:bottom-sheet:2.1.1")
+
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.android.compiler)
+ ksp(libs.androidx.hilt.compiler)
+
+ implementation(libs.scanner)
+ implementation(libs.client)
+
+ //implementation("no.nordicsemi.kotlin.ble:core:2.0.0-alpha02")
+ implementation("no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha02")
+
+ implementation(libs.accompanist.permissions)
+
+ implementation(libs.core)
+ implementation(libs.compose)
+ implementation(libs.compose.m3)
+
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+
+ implementation(libs.androidx.datastore.preferences)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlin.parcelize.runtime)
+
+ implementation(files("libs/poishadow-all.jar"))
+
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index bcd82a5..14b3fbb 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8c06f9b..e03ecb8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,7 +55,7 @@
+ android:theme="@style/Theme.Ble">
diff --git a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
index 10b9ca9..724e385 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt
@@ -15,6 +15,7 @@ import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -24,8 +25,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@@ -33,6 +36,7 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -72,9 +76,9 @@ class MainActivity : ComponentActivity() {
val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
- WindowCompat.setDecorFitsSystemWindows(window, false)
+ enableEdgeToEdge()
- installSplashScreen()
+ //installSplashScreen()
setContent {
@@ -103,7 +107,9 @@ class MainActivity : ComponentActivity() {
)
) {
- BoxWithConstraints {
+ BoxWithConstraints(
+ modifier = Modifier.navigationBarsPadding()
+ ) {
val maxHeight = with(LocalDensity.current) {
this@BoxWithConstraints.constraints.maxHeight.toDp()
@@ -168,86 +174,88 @@ class MainActivity : ComponentActivity() {
},
content = {
- Surface(
- modifier = Modifier
- .fillMaxSize()
- .navigationBarsPadding(),
- color = MaterialTheme.colorScheme.background
- ) {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .navigationBarsPadding(),
+ color = MaterialTheme.colorScheme.background
+ ) {
- val multiplePermissionsState =
+ val multiplePermissionsState =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ 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 {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
rememberMultiplePermissionsState(
listOf(
- Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.READ_MEDIA_VIDEO,
+ Manifest.permission.READ_MEDIA_IMAGES,
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 (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
+ )
+ )
+ }
}
+
+ var bleEnabled by remember {
+ mutableStateOf(mBluetoothAdapter.isEnabled)
}
- var bleEnabled by remember {
- mutableStateOf(mBluetoothAdapter.isEnabled)
- }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
- val lifecycleOwner = LocalLifecycleOwner.current
- val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
+ LaunchedEffect(lifecycleState) {
+ bleEnabled = mBluetoothAdapter.isEnabled
+ }
- LaunchedEffect(lifecycleState) {
- bleEnabled = mBluetoothAdapter.isEnabled
- }
+ if (multiplePermissionsState.allPermissionsGranted) {
- if (multiplePermissionsState.allPermissionsGranted) {
+ if (bleEnabled) {
- if (bleEnabled) {
+ MainScreen()
- MainScreen()
+ } else {
+
+ val context = LocalContext.current
+
+ LaunchedEffect(mBluetoothAdapter.isEnabled) {
+ val intent =
+ Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
+ (context as Activity).startActivityForResult(
+ intent,
+ 1
+ )
+ }
+ }
} else {
- val context = LocalContext.current
-
- LaunchedEffect(mBluetoothAdapter.isEnabled) {
- val intent =
- Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
- (context as Activity).startActivityForResult(
- intent,
- 1
- )
+ LaunchedEffect(multiplePermissionsState) {
+ multiplePermissionsState.launchMultiplePermissionRequest()
}
- }
- } else {
-
- LaunchedEffect(multiplePermissionsState) {
- multiplePermissionsState.launchMultiplePermissionRequest()
}
}
-
}
}
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
index 9f77c9e..bfd2513 100644
--- 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
@@ -17,19 +17,19 @@ import androidx.compose.ui.unit.dp
@Composable
fun SignalLevel(
modifier: Modifier = Modifier,
- maxLevel: Int = 5,
+ maxLevel: Int = 4,
level: Int
){
- val step = (16 - 4) / 4
+ val step = (16 - 4) / maxLevel
Row(
- modifier = modifier.height(16.dp),
+ modifier = modifier.height(12.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.Bottom
) {
- for(col in 0..4 step 1){
+ for(col in 0.. Unit,
-){
-
- var value by remember(tx) {
- mutableStateOf(tx)
- }
-
- Column(
- modifier = Modifier,
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Мощность",
- style = MaterialTheme.typography.titleLarge
- )
-
- Column(
- modifier = Modifier
- .weight(1f)
- .verticalScroll(rememberScrollState())
- ) {
-
- BleView.BleState.TX.entries.forEach {
-
- SelectorItem(
- label = "${it.value} dBb (${it.powerPercentage} %)",
- selected = it == value
- ){
- value = it
- }
-
- }
-
- }
-
- PrimaryButton(
- label = "Применить"
- ) {
- onSelect(value)
- }
-
- }
-
-}
\ 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 99a2710..e60a92a 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
@@ -49,16 +49,16 @@ class BleMapper @Inject constructor(
)
}
- is Ble.Host -> {
- BleView.Host(
+ is Ble.Gate -> {
+ BleView.Gate(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx),
version = input.state.version
),
- hostState = BleView.Host.HostState(
- historyInterval = input.hostState.historyInterval,
- readInterval = input.hostState.readInterval
+ hostState = BleView.Gate.HostState(
+ historyInterval = input.gateState.historyInterval,
+ readInterval = input.gateState.readInterval
)
)
}
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 f3c142d..7eeaded 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
@@ -49,14 +49,14 @@ class BleViewMapper @Inject constructor(
)
}
- is BleView.Host -> {
- Ble.Host(
+ is BleView.Gate -> {
+ Ble.Gate(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx),
version = input.state.version
),
- hostState = Ble.Host.HostState(
+ gateState = Ble.Gate.HostState(
historyInterval = input.hostState.historyInterval,
readInterval = input.hostState.readInterval
)
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 19950da..1e75bf9 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
@@ -60,7 +60,7 @@ sealed class BleView(
}
- class Host(
+ class Gate(
info: BleInfo,
val state: BleState,
val hostState: HostState
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
index aa14068..7287a01 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
@@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -21,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -35,71 +37,57 @@ fun BleInfoView(
version: BleRepositoryImpl.Version
) {
- Surface(
- modifier = Modifier.padding(bottom = 16.dp),
- shape = RoundedCornerShape(24.dp),
- color = MaterialTheme.colorScheme.surfaceVariant
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp)
) {
- Column(
- modifier = Modifier.padding(8.dp)
- ) {
-
- Column {
-
- BleInfoItem(
- icon = {
- Icon(
- imageVector = bleInfo.type.icon,
- contentDescription = null
- )
- },
- title = bleInfo.name,
- subtitle = "${bleInfo.type.localized} v${version}"
+ BleInfoItem(
+ shapeType = ShapeType.Start,
+ icon = {
+ Icon(
+ imageVector = bleInfo.type.icon,
+ contentDescription = null
)
+ },
+ title = bleInfo.name,
+ subtitle = "${bleInfo.type.localized} v${version}"
+ )
- SpecDivider()
-
- BleInfoItem(
- icon = {
- Icon(
- imageVector = Icons.Rounded.Key,
- contentDescription = null
- )
- },
- title = "Адрес",
- subtitle = bleInfo.serial
+ BleInfoItem(
+ shapeType = ShapeType.Middle,
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.Key,
+ contentDescription = null
)
+ },
+ title = "Адрес",
+ subtitle = bleInfo.serial
+ )
- SpecDivider()
-
- BleInfoItem(
- icon = {
- Icon(
- imageVector = Icons.Rounded.BatteryFull,
- contentDescription = null
- )
- },
- title = "Заряд батареи",
- subtitle = "${bleInfo.batteryLevel} %"
+ BleInfoItem(
+ shapeType = ShapeType.Middle,
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.BatteryFull,
+ contentDescription = null
)
+ },
+ title = "Заряд батареи",
+ subtitle = "${bleInfo.batteryLevel} %"
+ )
- SpecDivider()
-
- BleInfoItem(
- icon = {
- Icon(
- imageVector = Icons.Rounded.NetworkCell,
- contentDescription = null
- )
- },
- title = "Мощность сигнала",
- subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала"
+ BleInfoItem(
+ shapeType = ShapeType.End,
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.NetworkCell,
+ contentDescription = null
)
-
- }
-
- }
+ },
+ title = "Мощность сигнала",
+ subtitle = if (bleInfo.rssi != null) "${bleInfo.rssi} dBm" else "Нет сигнала"
+ )
}
@@ -116,48 +104,88 @@ private fun SpecDivider(){
}
+enum class ShapeType(
+ val shape: RoundedCornerShape
+) {
+
+ Start(RoundedCornerShape(16.dp, 16.dp, 4.dp, 4.dp)),
+ Middle(RoundedCornerShape(4.dp)),
+ End(RoundedCornerShape(4.dp, 4.dp, 16.dp, 16.dp)),
+ Singleton(RoundedCornerShape(16.dp));
+
+ companion object {
+
+ fun List.takeShapeType(item: T): ShapeType{
+ return if(size == 1){
+ ShapeType.Singleton
+ } else {
+ if(indexOf(item) == 0){
+ ShapeType.Start
+ } else {
+ if(indexOf(item) == size - 1){
+ ShapeType.End
+ } else {
+ ShapeType.Middle
+ }
+ }
+ }
+ }
+
+ }
+
+}
+
@Composable
private fun BleInfoItem(
+ shapeType: ShapeType,
icon: @Composable () -> Unit,
title: String,
subtitle: String
){
- Row(
- modifier = Modifier.padding(8.dp),
- verticalAlignment = Alignment.CenterVertically
+ Surface(
+ shape = shapeType.shape,
+ color = MaterialTheme.colorScheme.surfaceContainer
) {
- Surface(
- modifier = Modifier.size(40.dp),
- shape = CircleShape
+ Row(
+ modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ){
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ modifier = Modifier.size(36.dp),
+ ) {
- icon()
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+
+ icon()
+
+ }
}
- }
+ Spacer(modifier = Modifier.width(12.dp))
- Spacer(modifier = Modifier.width(12.dp))
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
- Column(
- modifier = Modifier.weight(1f)
- ) {
+ Text(
+ text = title
+ )
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = subtitle
+ )
- Text(
- text = title
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = subtitle
- )
+ }
}
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 db7fc29..24acbfe 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
@@ -5,15 +5,12 @@ import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
-import llc.arma.ble.domain.model.ConnectedBleInfo
class BleListContract {
sealed class Event : ViewEvent {
- data object OnResetFilter : Event()
-
- data object OnHideFilter : Event()
+ data object OnResetScanner : Event()
data object OnShowFilter : Event()
@@ -21,73 +18,32 @@ class BleListContract {
val bleAddress: String
) : Event()
- data class OnRssiRangeChanged(
- val rssi: ClosedFloatingPointRange
- ) : Event()
-
- data class OnBatteryRangeChanged(
- val battery: ClosedFloatingPointRange
- ) : Event()
-
- data class OnMacFilterChanged(
- val mac: String
- ) : Event()
-
- data class OnNameFilterChanged(
- val name: String
- ) : Event()
-
- data class OnTypeChanged(
- val type: BleInfo.Type?
- ) : Event()
-
- data class OnSortFieldChanged(
- val field: BleFilter.Field
- ) : Event()
-
- data class OnSortOrderChanged(
- val order: BleFilter.Order
- ) : Event()
-
}
data class State(
- val connectedBleList: List,
val bleList: List,
val bleFilter: BleFilter
- ) : ViewState {
-
- /*data class Filter(
- val sortField: Field = Field.Name,
- val sortOrder: Order = Order.Asc,
- val name: String = "",
- val mac: String = "",
- val battery: ClosedFloatingPointRange = (0f)..(100f),
- val rssi: ClosedFloatingPointRange = (-100f)..(-10f),
- val bleType: BleInfo.Type? = null
- ){
-
- enum class Field {
- Name, Mac, Distance, Dbm, Battery
- }
-
- enum class Order {
- Asc, Desc
- }
-
- }*/
-
- }
+ ) : ViewState
sealed class Effect : ViewSideEffect {
- object ShowFilter : Effect()
-
- object HideFilter : Effect()
-
sealed class Navigation : Effect() {
- data class NavigateToBle(
+ data object BleFilter : Navigation()
+
+ data class Beacon(
+ val serial: String
+ ) : Navigation()
+
+ data class Thermometer(
+ val serial: String
+ ) : Navigation()
+
+ data class Accelerometer(
+ val serial: String
+ ) : Navigation()
+
+ data class Gate(
val serial: String
) : Navigation()
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 8ad290e..03903fd 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
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -23,7 +24,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt
-import androidx.compose.material.icons.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows
import androidx.compose.material.icons.rounded.FilterAlt
@@ -34,11 +34,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
@@ -47,201 +50,222 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.BleFilterScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
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.app.ui.screen.ShapeType
+import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.icon
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
+@Destination(start = true)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BleListScreen(
- onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
+ //onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
+ navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel()
val state = viewModel.viewState.value
- val bottomDialog = rememberBottomDialogState()
-
val scrollState = rememberLazyListState()
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
+
+ LaunchedEffect(lifecycleState) {
+ if(lifecycleState == Lifecycle.State.RESUMED)
+ viewModel.setEvent(BleListContract.Event.OnResetScanner)
+ }
+
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
- is BleListContract.Effect.Navigation -> onNavigationEvent(it)
- is BleListContract.Effect.HideFilter -> launch {
- bottomDialog.hide()
- }
- is BleListContract.Effect.ShowFilter -> launch {
- bottomDialog.show {
- Filter(
- filter = viewModel.viewState.value.bleFilter,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- }
+ is BleListContract.Effect.Navigation.Accelerometer ->
+ navigator.navigate(AccelerometerScreenDestination(it.serial))
+
+ is BleListContract.Effect.Navigation.Beacon ->
+ navigator.navigate(BeaconScreenDestination(it.serial))
+
+ BleListContract.Effect.Navigation.BleFilter ->
+ navigator.navigate(BleFilterScreenDestination)
+
+ is BleListContract.Effect.Navigation.Gate ->
+ navigator.navigate(GateScreenDestination(it.serial))
+
+ is BleListContract.Effect.Navigation.Thermometer ->
+ navigator.navigate(ThermometerScreenDestination(it.serial))
+
}
}.launchIn(this)
}
- Column {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(text = "Arma BLE")
+ },
+ actions = {
- TopAppBar(
- modifier = Modifier
- .zIndex(1f)
- .shadow(if (scrollState.canScrollBackward) 8.dp else 0.dp),
- title = {
- Text(text = "Arma BLE")
- },
- actions = {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .align(Alignment.CenterVertically)
+ ) {
- Row(
- modifier = Modifier
- .padding(horizontal = 8.dp)
- .align(Alignment.CenterVertically)
- ) {
+ Text(text = "${state.bleList.size}")
- Text(text = "${state.bleList.size}")
-
- Spacer(modifier = Modifier.width(12.dp))
-
- Text(text = "${state.bleList.filter {
- it.batteryLevel == 100
- }.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
+ Spacer(modifier = Modifier.width(12.dp))
- Text(text = " | ")
+ Text(text = "${state.bleList.filter {
+ it.batteryLevel == 100
+ }.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
- Text(
- text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
- color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
- )
+ Text(text = " | ")
- Text(text = " | ")
+ Text(
+ text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
+ color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
+ )
- Text(
- text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
- color = MaterialTheme.colorScheme.error
- )
+ Text(text = " | ")
- }
+ Text(
+ text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
+ color = MaterialTheme.colorScheme.error
+ )
+
+ }
+
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BleListContract.Event.OnShowFilter)
+ }
+ ) {
+
+ Icon(
+ imageVector = Icons.Rounded.FilterAlt,
+ contentDescription = null
+ )
- IconButton(
- onClick = {
- viewModel.setEvent(BleListContract.Event.OnShowFilter)
- }
- ) {
-
- Icon(
- imageVector = Icons.Rounded.FilterAlt,
- contentDescription = null
- )
-
- }
-
- }
- )
-
- val filteredData = remember(state.bleList, state.bleFilter) {
-
- state.bleList.filter {
- (it.type == state.bleFilter.bleType || state.bleFilter.bleType == null) &&
- it.name.contains(state.bleFilter.name) &&
- it.serial.contains(state.bleFilter.mac) &&
- state.bleFilter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
- state.bleFilter.battery.contains(it.batteryLevel.toFloat())
- }.let {
- when (state.bleFilter.sortField) {
- BleFilter.Field.Name -> it.sortedBy { it.name }
- BleFilter.Field.Mac -> it.sortedBy { it.serial }
- BleFilter.Field.Distance -> it.sortedBy {
- 10.0.pow(
- (it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
- ).toFloat()
}
- BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
- BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
}
- }.let {
-
- when (state.bleFilter.sortOrder) {
- BleFilter.Order.Asc -> it
- BleFilter.Order.Desc -> it.reversed()
- }
-
- }
-
- }
-
- if(filteredData.isEmpty()){
- LinearProgressIndicator(
- strokeCap = StrokeCap.Round,
- modifier = Modifier
- .fillMaxWidth()
- .height(3.dp)
)
}
+ ) {
- if(filteredData.isEmpty()){
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
- Box(modifier = Modifier.fillMaxSize()){
- Text(
- modifier = Modifier.align(Alignment.Center),
- style = MaterialTheme.typography.titleMedium,
- text = "Метки в области не найдены"
+ val filteredData = remember(state.bleList, state.bleFilter) {
+
+ state.bleList.filter {
+ (it.type == state.bleFilter.bleType || state.bleFilter.bleType == null) &&
+ it.name.contains(state.bleFilter.name) &&
+ it.serial.contains(state.bleFilter.mac) &&
+ state.bleFilter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
+ state.bleFilter.battery.contains(it.batteryLevel.toFloat())
+ }.let {
+ when (state.bleFilter.sortField) {
+ BleFilter.Field.Name -> it.sortedBy { it.name }
+ BleFilter.Field.Mac -> it.sortedBy { it.serial }
+ BleFilter.Field.Distance -> it.sortedBy {
+ 10.0.pow(
+ (it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
+ ).toFloat()
+ }
+
+ BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
+ BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
+ }
+ }.let {
+
+ when (state.bleFilter.sortOrder) {
+ BleFilter.Order.Asc -> it
+ BleFilter.Order.Desc -> it.reversed()
+ }
+
+ }
+
+ }
+
+ if(filteredData.isEmpty()){
+ LinearProgressIndicator(
+ strokeCap = StrokeCap.Round,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(3.dp)
)
}
- } else {
+ if(filteredData.isEmpty()){
- LazyColumn(
- state = scrollState,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.fillMaxSize()
- ) {
+ Box(modifier = Modifier.fillMaxSize()){
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ style = MaterialTheme.typography.titleMedium,
+ text = "Метки в области не найдены"
+ )
+ }
- items(items = state.connectedBleList){
+ } else {
+
+ LazyColumn(
+ state = scrollState,
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+
+ items(
+ items = filteredData,
+ key = { it.serial }
+ ) {
+
+ BleItem(
+ ble = it,
+ shapeType = filteredData.takeShapeType(it),
+ onClick = {
+ viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
+ }
+ )
- ConnectedBleItem(ble = it) {
- viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
}
-
- items(
- items = filteredData,
- key = { it.serial }
- ) {
-
- BleItem(
- ble = it,
- onClick = {
- viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
- }
- )
-
- }
-
}
+
}
}
+
+
}
@Composable
@@ -251,7 +275,7 @@ fun ItemIcon(
Surface(
modifier = Modifier.size(40.dp),
- color = MaterialTheme.colorScheme.surfaceVariant,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
shape = CircleShape
) {
Box(modifier = Modifier.fillMaxSize()) {
@@ -275,6 +299,7 @@ private fun Int.toSignalLevel(): Int {
@Composable
fun BleItem(
+ shapeType: ShapeType,
ble: BleInfo,
onClick: () -> Unit
){
@@ -282,7 +307,7 @@ fun BleItem(
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
- MaterialTheme.colorScheme.background
+ MaterialTheme.colorScheme.surfaceContainer
}
val highAlpha = ContentAlpha.high
@@ -313,13 +338,10 @@ fun BleItem(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
- .clip(RoundedCornerShape(16.dp))
+ .clip(shapeType.shape)
.background(color)
.clickable(onClick = onClick)
- .padding(
- vertical = 8.dp,
- horizontal = 16.dp
- )
+ .padding(horizontal = 16.dp, vertical = 12.dp)
.alpha(alpha)
) {
@@ -366,64 +388,15 @@ fun BleItem(
Text(text = ble.name)
- Text(
- style = MaterialTheme.typography.bodyMedium,
- 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))
-
- val distance = remember(ble.rssi, ble.tx) {
- String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
- }
-
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = distance
- )
-
- }
-
- Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.alpha(0.7f)
- ) {
-
- SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
-
- Spacer(modifier = Modifier.width(4.dp))
-
- Box {
-
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = "-999 dBm",
- modifier = Modifier.alpha(0f)
- )
-
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = ble.rssi.toString() + " dBm"
- )
-
- }
-
- }
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = ble.serial
+ )
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -462,6 +435,64 @@ fun BleItem(
}
+
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+
+ 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))
+
+ val distance = remember(ble.rssi, ble.tx) {
+ String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
+ }
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = distance
+ )
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.alpha(0.7f)
+ ) {
+
+ SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
+
+ Spacer(modifier = Modifier.width(4.dp))
+
+ Box {
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = "-999 dBm",
+ modifier = Modifier.alpha(0f)
+ )
+
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = ble.rssi.toString() + " dBm"
+ )
+
+ }
+
+ }
+
+
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
@@ -508,62 +539,4 @@ fun BleItem(
}
-}
-
-@Composable
-private fun ConnectedBleItem(
- ble: ConnectedBleInfo,
- onClick: () -> Unit
-){
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(16.dp))
- .clickable(onClick = onClick)
- .background(MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = .99f))
- .padding(vertical = 8.dp, horizontal = 16.dp)
-
- ) {
-
- ItemIcon {
- Icon(
- modifier = Modifier.align(Alignment.Center),
- imageVector = Icons.Rounded.Link,
- contentDescription = null
- )
- }
-
- Column {
-
- Text(text = ble.name)
-
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = ble.serial
- )
-
- Row(
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.alpha(0.7f)
- ) {
-
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = "Соединено"
- )
-
- }
- }
-
- }
-
- }
-
}
\ No newline at end of file
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 685674e..28c6eac 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
@@ -8,40 +8,23 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter
+import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.GetBleAroundFlow
import llc.arma.ble.domain.usecase.filter.GetFilterFlow
-import llc.arma.ble.domain.usecase.filter.SaveFilter
import javax.inject.Inject
@HiltViewModel
class BleListViewModel @Inject constructor(
private val getFilterFlow: GetFilterFlow,
- private val saveFilter: SaveFilter,
- getBleAroundFlow: GetBleAroundFlow
+ private val getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel() {
+ private var scannerJob: Job? = null
+
init {
viewModelScope.launch {
- var job: Job? = null
-
- getBleAroundFlow().fold(
- onSuccess = {
- it.onEach {
- setState {
- copy(
- connectedBleList = emptyList(),
- bleList = it
- )
- }
- }.launchIn(viewModelScope)
- },
- onFailure = {
- throw IllegalStateException()
- }
- )
-
getFilterFlow.invoke().onEach {
setState {
copy(
@@ -49,51 +32,18 @@ class BleListViewModel @Inject constructor(
)
}
}.launchIn(this)
-
- /*while (true) {
-
- job?.cancel()
- job = getBleAroundFlow().onEach {
- it.fold(
- onSuccess = {
- setState {
- copy(
- connectedBleList = emptyList(),
- bleList = it
- )
- }
- },
- onFailure = {
-
- }
- )
-
- }.launchIn(viewModelScope)
-
- delay(30_000)
-
- }*/
-
}
}
override fun setInitialState(): BleListContract.State =
- BleListContract.State(emptyList(), emptyList(), BleFilter())
+ BleListContract.State(emptyList(), BleFilter())
override fun handleEvents(event: BleListContract.Event) {
when(event){
is BleListContract.Event.OnConnectToBle -> reduce(viewState.value, event)
- is BleListContract.Event.OnHideFilter -> reduce(viewState.value, event)
- is BleListContract.Event.OnMacFilterChanged -> reduce(viewState.value, event)
- is BleListContract.Event.OnNameFilterChanged -> reduce(viewState.value, event)
- is BleListContract.Event.OnResetFilter -> reduce(viewState.value, event)
- is BleListContract.Event.OnRssiRangeChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnShowFilter -> reduce(viewState.value, event)
- is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event)
- is BleListContract.Event.OnBatteryRangeChanged -> reduce(viewState.value, event)
- is BleListContract.Event.OnSortFieldChanged -> reduce(viewState.value, event)
- is BleListContract.Event.OnSortOrderChanged -> reduce(viewState.value, event)
+ is BleListContract.Event.OnResetScanner -> reduce(viewState.value, event)
}
}
@@ -101,159 +51,55 @@ class BleListViewModel @Inject constructor(
state: BleListContract.State,
event: BleListContract.Event.OnConnectToBle
) {
- setEffect {
- BleListContract.Effect.Navigation.NavigateToBle(serial = event.bleAddress)
- }
- }
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnHideFilter
- ) {
- setEffect {
- BleListContract.Effect.HideFilter
- }
- }
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnMacFilterChanged
- ) {
-
- setState {
- copy(bleFilter = state.bleFilter.copy(mac = event.mac))
- }
-
- viewModelScope.launch {
-
- saveFilter(
- bleFilter = state.bleFilter.copy(mac = event.mac)
- )
- }
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnSortOrderChanged
- ) {
-
- setState {
- copy(bleFilter = state.bleFilter.copy(sortOrder = event.order))
- }
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(sortOrder = event.order)
- )
- }
-
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnSortFieldChanged
- ) {
-
- setState {
- copy(bleFilter = state.bleFilter.copy(sortField = event.field))
- }
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(sortField = event.field)
- )
- }
-
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnNameFilterChanged
- ) {
-
- setState {
- copy(bleFilter = state.bleFilter.copy(name = event.name))
- }
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(name = event.name)
- )
- }
-
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnResetFilter
- ) {
-
- setState {
- copy(bleFilter = BleFilter())
- }
-
- viewModelScope.launch {
-
- saveFilter(BleFilter())
+ state.bleList.firstOrNull { it.serial == event.bleAddress }?.let { ble ->
setEffect {
- BleListContract.Effect.HideFilter
+
+ when (ble.type) {
+ BleInfo.Type.HOST ->
+ BleListContract.Effect.Navigation.Gate(serial = event.bleAddress)
+
+ BleInfo.Type.BEACON ->
+ BleListContract.Effect.Navigation.Beacon(serial = event.bleAddress)
+
+ BleInfo.Type.THERMOMETER ->
+ BleListContract.Effect.Navigation.Thermometer(serial = event.bleAddress)
+
+ BleInfo.Type.ACCELEROMETER ->
+ BleListContract.Effect.Navigation.Accelerometer(serial = event.bleAddress)
+ }
+
}
}
}
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnRssiRangeChanged
- ) {
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(rssi = event.rssi)
- )
- }
-
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnBatteryRangeChanged
- ) {
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(battery = event.battery)
- )
- }
-
- }
-
- private fun reduce(
- state: BleListContract.State,
- event: BleListContract.Event.OnTypeChanged
- ) {
-
- setState {
- copy(bleFilter = state.bleFilter.copy(bleType = event.type))
- }
-
- viewModelScope.launch {
- saveFilter(
- bleFilter = state.bleFilter.copy(bleType = event.type)
- )
- }
-
- }
-
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnShowFilter
) {
setEffect {
- BleListContract.Effect.ShowFilter
+ BleListContract.Effect.Navigation.BleFilter
}
}
+ private fun reduce(
+ state: BleListContract.State,
+ event: BleListContract.Event.OnResetScanner
+ ) {
+
+ scannerJob?.cancel()
+
+ scannerJob = getBleAroundFlow().onEach {
+ setState {
+ copy(
+ bleList = it
+ )
+ }
+ }.launchIn(viewModelScope)
+ }
+
}
\ No newline at end of file
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
deleted file mode 100644
index c0bcc81..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt
+++ /dev/null
@@ -1,472 +0,0 @@
-package llc.arma.ble.app.ui.screen.ble
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.BatteryFull
-import androidx.compose.material.icons.rounded.Bluetooth
-import androidx.compose.material.icons.rounded.Close
-import androidx.compose.material.icons.rounded.Search
-import androidx.compose.material.icons.rounded.ShortText
-import androidx.compose.material.icons.rounded.SignalCellularAlt
-import androidx.compose.material.icons.rounded.Sort
-import androidx.compose.material.icons.rounded.SortByAlpha
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.RangeSlider
-import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.common.SecondaryButton
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.model.BleFilter
-import llc.arma.ble.domain.model.BleInfo
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun Filter(
- filter: BleFilter,
- onEvent: (BleListContract.Event) -> Unit
-) {
-
- Column {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Фильтр",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(horizontal = 12.dp)
- .verticalScroll(rememberScrollState())
- ) {
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- Icon(
- imageVector = Icons.Rounded.Sort,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- var expanded by remember { mutableStateOf(false) }
-
- ExposedDropdownMenuBox(
- modifier = Modifier
- .fillMaxWidth()
- .padding(end = 8.dp),
- expanded = expanded,
- onExpandedChange = {
- expanded = it
- }
- ) {
-
- OutlinedTextField(
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth(),
- readOnly = true,
- value = filter.sortField.localized,
- onValueChange = { },
- label = { Text("Сортировка") },
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(
- expanded = expanded
- )
- }
- )
-
- ExposedDropdownMenu(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .fillMaxWidth(),
- expanded = expanded,
- onDismissRequest = {
- expanded = false
- }
- ) {
-
- BleFilter.Field.entries.forEach { selectionOption ->
- DropdownMenuItem(
- onClick = {
- onEvent(
- BleListContract.Event.OnSortFieldChanged(
- selectionOption
- )
- )
- expanded = false
- },
- text = {
- Text(text = selectionOption.localized)
- }
- )
- }
- }
- }
-
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- Icon(
- imageVector = Icons.Rounded.SortByAlpha,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- var expanded by remember { mutableStateOf(false) }
-
- ExposedDropdownMenuBox(
- modifier = Modifier
- .fillMaxWidth()
- .padding(end = 8.dp),
- expanded = expanded,
- onExpandedChange = {
- expanded = it
- }
- ) {
-
- OutlinedTextField(
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth(),
- readOnly = true,
- value = filter.sortOrder.localized,
- onValueChange = { },
- label = { Text("Напрвление сортировки") },
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(
- expanded = expanded
- )
- }
- )
-
- ExposedDropdownMenu(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .fillMaxWidth(),
- expanded = expanded,
- onDismissRequest = {
- expanded = false
- }
- ) {
-
- BleFilter.Order.entries.forEach { selectionOption ->
- DropdownMenuItem(
- onClick = {
- onEvent(
- BleListContract.Event.OnSortOrderChanged(
- selectionOption
- )
- )
- expanded = false
- },
- text = {
- Text(text = selectionOption.localized)
- }
- )
- }
- }
- }
-
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- Icon(
- imageVector = Icons.Rounded.Bluetooth,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- var expanded by remember { mutableStateOf(false) }
-
- ExposedDropdownMenuBox(
- modifier = Modifier
- .fillMaxWidth()
- .padding(end = 8.dp),
- expanded = expanded,
- onExpandedChange = {
- expanded = it
- }
- ) {
-
- OutlinedTextField(
- modifier = Modifier
- .menuAnchor()
- .fillMaxWidth(),
- readOnly = true,
- value = filter.bleType.localized,
- onValueChange = { },
- label = { Text("Тип") },
- trailingIcon = {
- ExposedDropdownMenuDefaults.TrailingIcon(
- expanded = expanded
- )
- }
- )
-
- ExposedDropdownMenu(
- modifier = Modifier
- .background(MaterialTheme.colorScheme.background)
- .fillMaxWidth(),
- expanded = expanded,
- onDismissRequest = {
- expanded = false
- }
- ) {
-
- mutableListOf(null).apply {
- addAll(BleInfo.Type.entries.toTypedArray())
- }.forEach { selectionOption ->
- DropdownMenuItem(
- onClick = {
- onEvent(
- BleListContract.Event.OnTypeChanged(
- selectionOption
- )
- )
- expanded = false
- },
- text = {
- Text(text = selectionOption.localized)
- }
- )
- }
- }
- }
-
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ){
-
- Icon(
- imageVector = Icons.Rounded.Search,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- OutlinedTextField(
- value = filter.name,
- singleLine = true,
- onValueChange = {
- onEvent(BleListContract.Event.OnNameFilterChanged(it))
- },
- label = {
- Text(text = "Имя")
- },
- trailingIcon = {
-
- if(filter.name.isNotEmpty()) {
-
- IconButton(
- onClick = { onEvent(BleListContract.Event.OnNameFilterChanged("")) }
- ) {
- Icon(
- imageVector = Icons.Rounded.Close,
- contentDescription = null
- )
- }
-
- }
-
- },
- modifier = Modifier
- .padding(end = 8.dp)
- .fillMaxWidth()
-
- )
-
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- Icon(
- imageVector = Icons.Rounded.ShortText,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- OutlinedTextField(
- value = filter.mac,
- singleLine = true,
- onValueChange = {
- onEvent(BleListContract.Event.OnMacFilterChanged(it))
- },
- label = {
- Text(text = "Mac")
- },
- trailingIcon = {
-
- if (filter.mac.isNotEmpty()) {
-
- IconButton(
- onClick = { onEvent(BleListContract.Event.OnMacFilterChanged("")) }
- ) {
- Icon(
- imageVector = Icons.Rounded.Close,
- contentDescription = null
- )
- }
-
- }
-
- },
- modifier = Modifier
- .padding(end = 8.dp)
- .fillMaxWidth()
- )
-
- }
-
- Spacer(modifier = Modifier.height(12.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .padding(end = 8.dp)
- ) {
-
- Icon(
- imageVector = Icons.Rounded.SignalCellularAlt,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- Column() {
-
- RangeSlider(
- value = filter.rssi,
- onValueChange = {
- onEvent(BleListContract.Event.OnRssiRangeChanged(it))
- },
- valueRange = (-100f)..(-10f),
- steps = 89,
- colors = SliderDefaults.colors(
- activeTickColor = MaterialTheme.colorScheme.primary,
- inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
- )
- )
-
- Row() {
-
- Text(text = filter.rssi.start.toInt().toString() + " dBm")
-
- Spacer(modifier = Modifier.weight(1f))
-
- Text(text = filter.rssi.endInclusive.toInt().toString() + " dBm")
-
- }
-
- }
-
- }
-
- Spacer(modifier = Modifier.height(12.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .padding(end = 8.dp)
- ) {
-
- Icon(
- imageVector = Icons.Rounded.BatteryFull,
- contentDescription = null,
- modifier = Modifier.padding(12.dp)
- )
-
- Column() {
-
- RangeSlider(
- value = filter.battery,
- onValueChange = {
- onEvent(BleListContract.Event.OnBatteryRangeChanged(it))
- },
- valueRange = 0f..100f,
- steps = 99,
- colors = SliderDefaults.colors(
- activeTickColor = MaterialTheme.colorScheme.primary,
- inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
- )
- )
-
- Row {
-
- Text(text = "0 %")
-
- Spacer(modifier = Modifier.weight(1f))
-
- Text(text = "100 %")
-
- }
-
- }
-
- }
-
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(BleListContract.Event.OnHideFilter)
- }
-
- SecondaryButton(
- label = "Сбросить"
- ) {
- onEvent(BleListContract.Event.OnResetFilter)
- }
-
- }
-
-}
\ No newline at end of file
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 ca775fa..c5cd83f 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
@@ -5,10 +5,10 @@ import kotlinx.parcelize.Parcelize
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.screen.inspection.accelerometer.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
+import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
+import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
@@ -31,7 +31,7 @@ class ConnectionContract {
) : Event()
data class OnHostNavigationEvent(
- val event: HostContract.Effect.Navigation
+ val event: GateContract.Effect.Navigation
) : Event()
data class OnThermometerNavigationEvent(
@@ -50,6 +50,7 @@ class ConnectionContract {
data object Loading : State()
data class DisplayException(
+ val tries: Long,
val exception: BleException
) : State()
@@ -73,6 +74,10 @@ class ConnectionContract {
val serial: String
) : Navigation()
+ data class NavigateToThermometerHistory(
+ val bleSerial: String
+ ) : Navigation()
+
}
sealed class InnerNavigation : Effect() {
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 3da1dbe..948ee7b 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
@@ -2,31 +2,28 @@ package llc.arma.ble.app.ui.screen.connection
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
-import androidx.compose.animation.with
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.ArrowBack
-import androidx.compose.material3.CenterAlignedTopAppBar
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -35,24 +32,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.SmallPrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerScreen
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerHistory
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerRealtime
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerSpectre
-import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconScreen
-import llc.arma.ble.app.ui.screen.inspection.host.HostScreen
-import llc.arma.ble.app.ui.screen.inspection.host.view.HostHistory
-import llc.arma.ble.app.ui.screen.inspection.host.view.table.BleTableEditContract
-import llc.arma.ble.app.ui.screen.inspection.host.view.table.BleTableEditScreen
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerScreen
-import llc.arma.ble.domain.model.Ble
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -86,7 +71,7 @@ fun ConnectionScreen(
Column {
- CenterAlignedTopAppBar(
+ TopAppBar(
navigationIcon = {
IconButton(
onClick = {
@@ -127,18 +112,17 @@ fun ConnectionScreen(
}
)
- when (state) {
+ /*when (state) {
is ConnectionContract.State.DisplayException -> DisplayException(
- onEvent = {
- viewModel.setEvent(it)
- }
+ viewState = state,
+ onEvent = viewModel::setEvent
)
is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> {
when (state.ble) {
is Ble.Beacon -> BeaconScreen(
- ble = state.ble,
+ null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnBeaconNavigationEvent(
@@ -151,7 +135,8 @@ fun ConnectionScreen(
is Ble.Thermometer -> {
ThermometerScreen(
- ble = state.ble,
+ txSelectResult = null,
+ //ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnThermometerNavigationEvent(
@@ -164,16 +149,17 @@ fun ConnectionScreen(
}
is Ble.Accelerometer -> {
- AccelerometerScreen(ble = state.ble) {
+ /*AccelerometerScreen {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
- }
+ }*/
}
- is Ble.Host -> {
- HostScreen(
- ble = state.ble,
+ is Ble.Gate -> {
+ GateScreen(
+ null,
+ null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnHostNavigationEvent(it)
@@ -186,10 +172,12 @@ fun ConnectionScreen(
}
- }
+ }*/
}
+ /*
+
innerScreen?.let {
Surface(
@@ -245,20 +233,20 @@ fun ConnectionScreen(
}
is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> {
- HostHistory(
+ /*GateHistoryScreen(
ble = it.ble,
onDismiss = {
innerScreen = null
}
- )
+ )*/
}
is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> {
- BleTableEditScreen(it.serial){
+ GateBleTableScreen {
when(it){
- BleTableEditContract.Effect.Navigation.NavigateUp -> {
+ GateBleTableContract.Effect.Navigation.Up -> {
innerScreen = null
}
}
@@ -272,26 +260,28 @@ fun ConnectionScreen(
}
+ */
+
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
- Column {
- Box(modifier = Modifier.fillMaxSize()) {
- CircularProgressIndicator(
- strokeCap = StrokeCap.Round,
- modifier = Modifier.align(Alignment.Center
- )
- )
- }
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ ContainedLoadingIndicator()
}
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun DisplayException(
+ viewState: ConnectionContract.State.DisplayException,
onEvent: (ConnectionContract.Event) -> Unit
){
@@ -301,9 +291,20 @@ private fun DisplayException(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.align(Alignment.Center)
+ modifier = Modifier.align(Alignment.Center).widthIn(max = 270.dp)
) {
+
+ ContainedLoadingIndicator()
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleMedium,
+ text = "Повторная попытка ${viewState.tries}"
+ )
+
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
@@ -313,9 +314,9 @@ private fun DisplayException(
Spacer(modifier = Modifier.height(18.dp))
SmallPrimaryButton(
- label = "Повторить"
+ label = "Отмена"
) {
- onEvent(ConnectionContract.Event.RefreshBle)
+ onEvent(ConnectionContract.Event.OnNavigateUp)
}
}
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 0925cd7..b91d837 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
@@ -7,10 +7,7 @@ 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.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.usecase.GetBleBySerial
import javax.inject.Inject
@@ -41,19 +38,19 @@ class ConnectionViewModel @Inject constructor(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnHostNavigationEvent
) {
- when(event.event){
- HostContract.Effect.Navigation.NavigateUp -> {
+ /*when(event.event){
+ GateContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
- HostContract.Effect.Navigation.NavigateToChangePassword -> {
+ is GateContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
}
}
- is HostContract.Effect.Navigation.NavigateToHostHistory -> {
+ is GateContract.Effect.Navigation.GateHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble
@@ -61,14 +58,17 @@ class ConnectionViewModel @Inject constructor(
}
}
- is HostContract.Effect.Navigation.NavigateToBleTable -> {
+ is GateContract.Effect.Navigation.BleTable -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial
)
}
}
- }
+
+ is GateContract.Effect.Navigation.TxSelector -> TODO()
+ is GateContract.Effect.Navigation.ReadIntervalSelector -> TODO()
+ }*/
}
private fun reduce(
@@ -76,16 +76,18 @@ class ConnectionViewModel @Inject constructor(
event: ConnectionContract.Event.OnBeaconNavigationEvent
) {
when(event.event){
- BeaconContract.Effect.Navigation.NavigateUp -> {
+ BeaconContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
- BeaconContract.Effect.Navigation.NavigateToChangePassword -> {
+ is BeaconContract.Effect.Navigation.PasswordForm -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
}
}
+
+ is BeaconContract.Effect.Navigation.TxSelector -> TODO()
}
}
@@ -93,32 +95,40 @@ class ConnectionViewModel @Inject constructor(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnThermometerNavigationEvent
) {
- when(event.event){
- ThermometerContract.Effect.Navigation.NavigateUp -> {
+ /*(event.event){
+ ThermometerContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
- ThermometerContract.Effect.Navigation.NavigateToChangePassword -> {
+ ThermometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
}
}
- }
+
+ is ThermometerContract.Effect.Navigation.ThermometerHistory -> {
+ setEffect {
+ ConnectionContract.Effect.Navigation.NavigateToThermometerHistory(event.event.bleSerial)
+ }
+ }
+
+ is ThermometerContract.Effect.Navigation.TxSelector -> TODO()
+ }*/
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
- when(event.event){
- AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> {
+ /*when(event.event){
+ is AccelerometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get("serial")!!)
}
}
- is AccelerometerContract.Effect.Navigation.NavigateToAccelHistory -> {
+ is AccelerometerContract.Effect.Navigation.AccelHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory(
event.event.ble,
@@ -131,7 +141,7 @@ class ConnectionViewModel @Inject constructor(
}
}
- is AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime -> {
+ is AccelerometerContract.Effect.Navigation.AccelRealtime -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime(
event.event.ble,
@@ -143,7 +153,7 @@ class ConnectionViewModel @Inject constructor(
)
}
}
- is AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre -> {
+ is AccelerometerContract.Effect.Navigation.AccelSpectre -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre(
event.event.ble,
@@ -155,7 +165,12 @@ class ConnectionViewModel @Inject constructor(
)
}
}
+
+ is AccelerometerContract.Effect.Navigation.TxPowerSelector -> TODO()
+ is AccelerometerContract.Effect.Navigation.ReadIntervalSelector -> TODO()
+ is AccelerometerContract.Effect.Navigation.SaveIntervalSelector -> TODO()
}
+ */
}
private fun reduce(
@@ -179,7 +194,7 @@ class ConnectionViewModel @Inject constructor(
}
private fun refreshBle(){
- val serial = savedStateHandle.get("serial")
+ /*val serial = savedStateHandle.get("serial")
if(serial != null){
@@ -189,28 +204,37 @@ class ConnectionViewModel @Inject constructor(
ConnectionContract.State.Loading
}
- getBleBySerial(serial).fold(
- onSuccess = {
+ var tries = 0L
- it.onEach {
+ while (true) {
+
+ getBleBySerial(serial, this).fold(
+ onSuccess = {
+
+ it.onEach {
+ setState {
+ ConnectionContract.State.Display(
+ ble = it
+ )
+ }
+ }.launchIn(viewModelScope)
+
+ return@launch
+
+ },
+ onFailure = {
setState {
- ConnectionContract.State.Display(
- ble = it
- )
+ tries += 1
+ ConnectionContract.State.DisplayException(tries, it)
}
- }.launchIn(viewModelScope)
-
- },
- onFailure = {
- setState {
- ConnectionContract.State.DisplayException(it)
}
- }
- )
+ )
+
+ }
}
} else {
throw IllegalArgumentException("serial arg must not be null")
- }
+ }*/
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterContract.kt
new file mode 100644
index 0000000..7d3f094
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterContract.kt
@@ -0,0 +1,44 @@
+package llc.arma.ble.app.ui.screen.filter
+
+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.BleFilter
+
+class BleFilterContract {
+
+ sealed class Event : ViewEvent {
+
+ data class OnFilterChanged(
+ val filter: BleFilter
+ ) : Event()
+
+ data object OnSave : Event()
+
+ data object OnResetFilter : Event()
+
+ data object OnNavigateUp : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data object Loading : State()
+
+ data class Display(
+ val filter: BleFilter
+ ) : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect(){
+
+ data object Up : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterScreen.kt
new file mode 100644
index 0000000..0d7eee3
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterScreen.kt
@@ -0,0 +1,574 @@
+package llc.arma.ble.app.ui.screen.filter
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.input.TextFieldLineLimits
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ShortText
+import androidx.compose.material.icons.automirrored.rounded.Sort
+import androidx.compose.material.icons.rounded.BatteryFull
+import androidx.compose.material.icons.rounded.Bluetooth
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material.icons.rounded.Restore
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material.icons.rounded.SignalCellularAlt
+import androidx.compose.material.icons.rounded.SortByAlpha
+import androidx.compose.material3.Button
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.RangeSlider
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleFilter
+import llc.arma.ble.domain.model.BleInfo
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BleFilterScreen(
+ navigator: DestinationsNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect {
+ when(it){
+ BleFilterContract.Effect.Navigation.Up -> navigator.popBackStack()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text("Фильтр BLE")
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnResetFilter)
+ }
+ ) {
+
+ Icon(
+ imageVector = Icons.Rounded.Restore,
+ contentDescription = null
+ )
+
+ }
+ }
+ )
+ }
+ ) {
+
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when(state){
+ is BleFilterContract.State.Display -> DisplayState(viewModel, state)
+ is BleFilterContract.State.Loading -> LoadingState()
+ }
+
+ }
+
+ }
+
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun LoadingState(){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
+
+ ContainedLoadingIndicator()
+
+ }
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DisplayState(
+ viewModel: BleFilterViewModel,
+ state: BleFilterContract.State.Display
+){
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
+
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(16.dp)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ state.filter.sortField.localized,
+ TextRange(state.filter.sortField.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ }
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Сортировка") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.Sort,
+ contentDescription = null,
+ modifier = Modifier.padding(12.dp)
+ )
+ },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ BleFilter.Field.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(
+ sortField = selectionOption
+ )
+ ))
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+ val sortTextFieldState = TextFieldState(
+ state.filter.sortOrder.localized,
+ TextRange(state.filter.sortOrder.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ }
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFieldState,
+ readOnly = true,
+ label = { Text("Напрвление сортировки") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.SortByAlpha,
+ contentDescription = null,
+ )
+ },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+
+ BleFilter.Order.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(sortOrder = selectionOption)
+ ))
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+ val typeTextFiledState = TextFieldState(
+ state.filter.bleType.localized,
+ TextRange(state.filter.bleType.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ }
+ ) {
+
+ OutlinedTextField(
+ state = typeTextFiledState,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ readOnly = true,
+ label = { Text("Тип") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Bluetooth,
+ contentDescription = null
+ )
+ },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+
+ mutableListOf(null).apply {
+ addAll(BleInfo.Type.entries.toTypedArray())
+ }.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(bleType = selectionOption)
+ ))
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ OutlinedTextField(
+ value = state.filter.name,
+ singleLine = true,
+ onValueChange = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(name = it)
+ ))
+ },
+ label = { Text(text = "Имя") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = null
+ )
+ },
+ trailingIcon = {
+
+ if(state.filter.name.isNotEmpty()) {
+
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(name = "")
+ ))
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = null
+ )
+ }
+
+ }
+
+ },
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .fillMaxWidth()
+
+ )
+
+ OutlinedTextField(
+ value = state.filter.mac,
+ singleLine = true,
+ onValueChange = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(mac = it)
+ ))
+ },
+ label = { Text(text = "Mac") },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ShortText,
+ contentDescription = null,
+ )
+ },
+ trailingIcon = {
+
+ if (state.filter.mac.isNotEmpty()) {
+
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(mac = "")
+ ))
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Close,
+ contentDescription = null
+ )
+ }
+
+ }
+
+ },
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .fillMaxWidth()
+ )
+
+ }
+
+ }
+
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(16.dp)
+ ) {
+
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+
+ Icon(
+ imageVector = Icons.Rounded.SignalCellularAlt,
+ contentDescription = null,
+ modifier = Modifier.padding(12.dp)
+ )
+
+ Column {
+
+ RangeSlider(
+ value = state.filter.rssi,
+ onValueChange = {
+ viewModel.setEvent(
+ BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(rssi = it)
+ )
+ )
+ },
+ valueRange = (-100f)..(-10f),
+ colors = SliderDefaults.colors(
+ activeTickColor = MaterialTheme.colorScheme.primary,
+ inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
+ )
+ )
+
+ Row {
+
+ Text(text = state.filter.rssi.start.toInt().toString() + " dBm")
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = state.filter.rssi.endInclusive.toInt().toString() + " dBm")
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+
+ Icon(
+ imageVector = Icons.Rounded.BatteryFull,
+ contentDescription = null,
+ modifier = Modifier.padding(12.dp)
+ )
+
+ Column(
+ verticalArrangement = Arrangement.Center
+ ) {
+
+ RangeSlider(
+ value = state.filter.battery,
+ onValueChange = {
+ viewModel.setEvent(
+ BleFilterContract.Event.OnFilterChanged(
+ state.filter.copy(battery = it)
+ )
+ )
+ },
+ valueRange = 0f..100f,
+ colors = SliderDefaults.colors(
+ activeTickColor = MaterialTheme.colorScheme.primary,
+ inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
+ )
+ )
+
+ Row {
+
+ Text(text = state.filter.battery.start.toInt().toString() + " %")
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ Text(text = state.filter.battery.endInclusive.toInt().toString() + " %")
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Button(
+ onClick = {
+ viewModel.setEvent(BleFilterContract.Event.OnSave)
+ },
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Text(
+ text = "Применить"
+ )
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterViewModel.kt
new file mode 100644
index 0000000..c115c1e
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/filter/BleFilterViewModel.kt
@@ -0,0 +1,98 @@
+package llc.arma.ble.app.ui.screen.filter
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.domain.model.BleFilter
+import llc.arma.ble.domain.usecase.filter.GetFilterFlow
+import llc.arma.ble.domain.usecase.filter.SaveFilter
+import javax.inject.Inject
+
+@HiltViewModel
+class BleFilterViewModel @Inject constructor(
+ private val getFilterFlow: GetFilterFlow,
+ private val saveFilter: SaveFilter
+) : BaseViewModel() {
+
+ init {
+
+ viewModelScope.launch {
+
+ val filter = getFilterFlow.invoke().firstOrNull() ?: BleFilter()
+
+ setState { BleFilterContract.State.Display(filter) }
+
+ }
+
+ }
+
+ override fun setInitialState() = BleFilterContract.State.Loading
+
+ override fun handleEvents(event: BleFilterContract.Event) {
+ when(event){
+ is BleFilterContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ is BleFilterContract.Event.OnSave -> reduce(viewState.value, event)
+ is BleFilterContract.Event.OnFilterChanged -> reduce(viewState.value, event)
+ is BleFilterContract.Event.OnResetFilter -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: BleFilterContract.State,
+ event: BleFilterContract.Event.OnNavigateUp,
+ ) {
+ setEffect {
+ BleFilterContract.Effect.Navigation.Up
+ }
+ }
+
+ private fun reduce(
+ state: BleFilterContract.State,
+ event: BleFilterContract.Event.OnSave,
+ ) {
+
+ if(state is BleFilterContract.State.Display) {
+
+ viewModelScope.launch {
+
+ saveFilter(state.filter)
+
+ setEffect {
+ BleFilterContract.Effect.Navigation.Up
+ }
+
+
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BleFilterContract.State,
+ event: BleFilterContract.Event.OnFilterChanged,
+ ) {
+ setState { BleFilterContract.State.Display(event.filter) }
+ }
+
+ private fun reduce(
+ state: BleFilterContract.State,
+ event: BleFilterContract.Event.OnResetFilter,
+ ) {
+
+ viewModelScope.launch {
+
+ saveFilter(BleFilter())
+
+ setEffect {
+ BleFilterContract.Effect.Navigation.Up
+ }
+
+
+ }
+
+ }
+
+}
\ No newline at end of file
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
deleted file mode 100644
index 99749e4..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerContract.kt
+++ /dev/null
@@ -1,223 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer
-
-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.view.RealtimeViewMode
-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
-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 OnShowAccelerometerAccel : Event()
- object OnHideAccelerometerAccel : Event()
-
- object OnHideAccelerometerSpectre : Event()
-
- object OnShowAccelerometerHistory : Event()
- object OnHideAccelerometerHistory : Event()
-
- object OnRealtimeViewModeEdit : Event()
-
- data class OnAccelViewModeEdit(
- val next: Next
- ) : Event()
-
- enum class Next {
- ACCEL, HISTORY
- }
-
- data class OnAccelScaleEdit(
- val next: Next
- ) : Event()
-
- object OnAccelEdit : 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 OnReadIntervalEdit : Event()
- object OnHideHistoryEdit : Event()
-
- data class OnRealtimeViewModeChanged(
- val mode: RealtimeViewMode
- ) : 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()
-
- data class OnReadIntervalChanged(
- val interval: Long
- ) : Event()
-
- }
-
- sealed class State : ViewState {
-
- object Loading : State()
-
- data class Display(
- val origin: Ble.Accelerometer,
- val accelerometer: BleView.Accelerometer,
- val writeState: WriteState?,
- val accelViewMode: AccelViewMode,
- val accelRealtimeViewMode: RealtimeViewMode,
- 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 ShowRealtimeViewModeEdit : 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 HideAccelEdit : Effect()
-
- object ShowFftFrequencyEdit : Effect()
- object ShowFftAxisEdit : Effect()
- object ShowFftModeEdit : Effect()
-
- object HideIntervalPicker : Effect()
- object ShowIntervalPicker : Effect()
-
- object HideReadIntervalPicker : Effect()
- object ShowReadIntervalPicker : Effect()
-
- object ShowHistoryEdit : Effect()
-
- sealed class Navigation : Effect() {
-
- object NavigateToChangePassword : Navigation()
-
- data class NavigateToAccelHistory(
- val ble: BleInfo,
- val accelScale: AccelScale,
- val accelMode: AccelViewMode,
- val fftAxis: FftAxis,
- val fftMode: FftViewMode,
- val frequency: FftFrequency
- ) : Navigation()
-
- data class NavigateToAccelRealtime(
- val ble: BleInfo,
- val accelScale: AccelScale,
- val accelMode: AccelViewMode,
- val fftAxis: FftAxis,
- val fftMode: FftViewMode,
- val frequency: FftFrequency
- ) : Navigation()
-
- data class NavigateToAccelSpectre(
- val ble: BleInfo,
- val accelScale: AccelScale,
- val accelMode: AccelViewMode,
- val fftAxis: FftAxis,
- val fftMode: FftViewMode,
- val frequency: FftFrequency
- ) : Navigation()
-
- }
-
- }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index 3eeddcb..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerScreen.kt
+++ /dev/null
@@ -1,415 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.ModalBottomSheetValue
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.hilt.navigation.compose.hiltViewModel
-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.TxLevelSelector
-import llc.arma.ble.app.ui.common.rememberBottomDialogState
-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.AccelRealtimeViewEdit
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelScaleEdit
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelViewEdit
-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.ReadIntervalEdit
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.Write
-import llc.arma.ble.domain.model.Ble
-
-enum class SheetPage {
- ACCEL_SCALE, SPECTRE_SCALE, HISTORY_MODE_EDIT, HISTORY_SCALE, HISTORY_EDIT, ACCEL_EDIT, ACCEL_REALTIME_EDIT, POWER, WRITE, ACCEL_MODE_EDIT, SPECTRE_MODE_EDIT, FREQUENCY_EDIT, AXIS_EDIT, FFT_MODE_EDIT, INTERVAL_EDIT, READ_INTERVAL_EDIT
-}
-
-@OptIn(ExperimentalMaterialApi::class)
-@Composable
-fun AccelerometerScreen(
- ble: Ble.Accelerometer,
- onEvent: (AccelerometerContract.Effect.Navigation) -> Unit
-) {
-
- val viewModel = hiltViewModel()
- val state = viewModel.viewState.value
-
- val bottomDialog = rememberBottomDialogState()
-
- LaunchedEffect(ble){
- viewModel.setEvent(AccelerometerContract.Event.OnBleChanged(ble))
- }
-
- var sheetPage by rememberSaveable {
- 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.POWER -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- TxLevelSelector(
- tx = currentState.accelerometer.state.tx,
- onSelect = {
- viewModel.setEvent(AccelerometerContract.Event.OnPowerChanged(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
- )
-
- }
-
- }
- }
- 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
- )
- }
-
- }
- SheetPage.SPECTRE_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
- )
- }
-
- }
- 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
- )
- }
-
- }
- SheetPage.FREQUENCY_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- AccelFrequencyEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- SheetPage.AXIS_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- AccelFftAxisEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- SheetPage.FFT_MODE_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- AccelFftModeEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- SheetPage.INTERVAL_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- IntervalEdit(
- state = currentState.accelerometer,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- 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
- )
- }
-
- }
- SheetPage.SPECTRE_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
- )
- }
-
- }
- 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
- )
- }
-
- }
- SheetPage.ACCEL_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- AccelEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- SheetPage.HISTORY_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- HistoryEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- null -> {
- bottomDialog.hide()
- }
-
- SheetPage.ACCEL_REALTIME_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- AccelRealtimeViewEdit(
- state = currentState,
- onEvent = viewModel::setEvent
- )
- }
-
- }
-
- SheetPage.READ_INTERVAL_EDIT -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is AccelerometerContract.State.Display) {
- ReadIntervalEdit(
- state = currentState.accelerometer,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- }
- }
-
- DisposableEffect(key1 = Unit, effect = {
- onDispose {
- scope.launch {
- bottomDialog.hide()
- }
- }
- })
-
- LaunchedEffect("effect"){
- viewModel.effect.onEach {
- when(it){
- 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.ShowAccelViewEdit -> launch {
- sheetPage = null
- delay(100)
- sheetPage = when(it.next){
- AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_MODE_EDIT
- AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_MODE_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.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.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)
- }
-
- AccelerometerContract.Effect.ShowRealtimeViewModeEdit -> {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.ACCEL_REALTIME_EDIT
- }
-
- AccelerometerContract.Effect.HideAccelEdit -> {
- sheetPage = null
- delay(100)
- }
-
- AccelerometerContract.Effect.HideReadIntervalPicker -> {
- sheetPage = null
- delay(100)
- }
-
- AccelerometerContract.Effect.ShowReadIntervalPicker -> {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.READ_INTERVAL_EDIT
- }
-
- }
- }.launchIn(this)
- }
-
- Column {
-
- when(state){
- is AccelerometerContract.State.Display -> {
- DisplayState(
- origin = state.origin,
- ble = state.accelerometer,
- onEvent = viewModel::setEvent
- )
- }
- is AccelerometerContract.State.Loading -> LoadingState()
- }
-
- }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index dc11b14..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/AccelerometerViewModel.kt
+++ /dev/null
@@ -1,703 +0,0 @@
-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.accelerometer.view.RealtimeViewMode
-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 bleViewMapper: BleViewMapper,
- private val writeBle: WriteBle
-) : BaseViewModel() {
-
- override fun setInitialState() = AccelerometerContract.State.Loading
-
- override fun handleEvents(event: AccelerometerContract.Event) {
- when(event){
- is AccelerometerContract.Event.OnBleChanged -> 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.OnAccelViewModeEdit -> reduce(viewState.value, event)
- is AccelerometerContract.Event.OnAccelViewModelChanged -> 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.OnRealtimeViewModeEdit -> reduce(viewState.value, event)
- is AccelerometerContract.Event.OnRealtimeViewModeChanged -> 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)
- is AccelerometerContract.Event.OnReadIntervalChanged -> reduce(viewState.value, event)
- is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
- }
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnReadIntervalChanged
- ) {
-
- if(state is AccelerometerContract.State.Display) {
-
- state.accelerometer.accelerometerState.readInterval = event.interval
-
- }
-
- setEffect {
- AccelerometerContract.Effect.HideReadIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnReadIntervalEdit
- ) {
-
- setEffect {
- AccelerometerContract.Effect.ShowReadIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnHideHistoryEdit
- ) {
-
- setEffect {
- 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.HistorySettings.Enabled){
- saveHistory = Ble.Accelerometer.HistorySettings.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.HistorySettings.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.HistorySettings.Enabled(
- scale = AccelScale.S_2,
- mode = AccelViewMode.ACCELERATION,
- detailed = true
- )
-
- setEffect {
- AccelerometerContract.Effect.ShowHistoryEdit
- }
-
- } else {
-
- state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.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.OnRealtimeViewModeEdit
- ) {
-
- setEffect {
- AccelerometerContract.Effect.ShowRealtimeViewModeEdit
- }
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnRealtimeViewModeChanged
- ) {
-
- if(state is AccelerometerContract.State.Display){
-
- setState {
-
- state.copy(
- accelRealtimeViewMode = event.mode
- )
-
- }
-
- }
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnHideAccelerometerSpectre
- ) {
-
-
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnShowAccelerometerHistory
- ) {
-
- /*setEffect {
- AccelerometerContract.Effect.ShowAccelerometerHistory
- }*/
-
-
- if (state is AccelerometerContract.State.Display &&
- state.origin.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled
- ) {
- setEffect {
-
- AccelerometerContract.Effect.Navigation.NavigateToAccelHistory(
- ble = state.accelerometer.info,
- accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
- fftAxis = state.fftAxis,
- fftMode = state.fftViewMode,
- frequency = state.fftFrequency,
- accelScale = state.accelScale
- )
- }
-
- }
-
- }
-
- 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,
- saveHistorySettings = if(newBle.accelerometerState.saveHistorySettings == state.origin.accelerometerState.saveHistorySettings) null else newBle.accelerometerState.saveHistorySettings,
- historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
- readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval,
- )
-
- 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 {
-
- if(state is AccelerometerContract.State.Display){
-
- /*setEffect {
-
- AccelerometerContract.Effect.ShowAccelerometerAccel
-
- }*/
-
- setEffect {
-
- AccelerometerContract.Effect.HideAccelEdit
-
- }
-
- setEffect {
-
- when (state.accelRealtimeViewMode) {
- is RealtimeViewMode.Accel -> {
-
- AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime(
- ble = state.accelerometer.info,
- accelMode = state.accelRealtimeViewMode.accelViewMode,
- fftAxis = state.fftAxis,
- fftMode = state.fftViewMode,
- frequency = state.fftFrequency,
- accelScale = state.accelScale
- )
-
- }
-
- is RealtimeViewMode.Spectre -> {
-
- AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre(
- ble = state.accelerometer.info,
- accelMode = state.accelViewMode,
- fftAxis = state.fftAxis,
- fftMode = state.fftViewMode,
- frequency = state.fftFrequency,
- accelScale = state.accelScale
- )
-
- }
- }
-
- }
-
- }
-
- }
-
- }
-
- private fun reduce(
- state: AccelerometerContract.State,
- event: AccelerometerContract.Event.OnBleChanged
- ) {
-
- when (state) {
- is AccelerometerContract.State.Display -> setState {
- state.copy(
- origin = Ble.Accelerometer(
- info = event.ble.info,
- state = event.ble.state,
- accelerometerState = state.origin.accelerometerState
- )
- )
- }
-
- is AccelerometerContract.State.Loading -> setState {
- AccelerometerContract.State.Display(
- origin = event.ble,
- accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer,
- writeState = null,
- accelViewMode = AccelViewMode.ACCELERATION,
- accelRealtimeViewMode = RealtimeViewMode.Accel(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(
- saveHistorySettings = request.writeRequest.saveHistorySettings
- ?: currentState.origin.accelerometerState.saveHistorySettings
- )
- )
-
- 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/history/form/AccelerometerHistoryForm.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/form/AccelerometerHistoryForm.kt
new file mode 100644
index 0000000..e60d5a1
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/form/AccelerometerHistoryForm.kt
@@ -0,0 +1,219 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.input.TextFieldLineLimits
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.dp
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.result.ResultBackNavigator
+import com.ramcosta.composedestinations.spec.DestinationStyle
+import kotlinx.serialization.Serializable
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.usecase.AccelScale
+import llc.arma.ble.domain.usecase.AccelViewMode
+
+@Serializable
+data class AccelerometerHistoryFormData(
+ val mode: AccelViewMode,
+ val scale: AccelScale
+)
+
+@Destination(style = DestinationStyle.Dialog::class)
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AccelerometerHistoryForm(
+ resultNavigator: ResultBackNavigator
+) {
+
+ var mode by remember { mutableStateOf(AccelViewMode.entries.first()) }
+ var scale by remember { mutableStateOf(AccelScale.entries.first()) }
+
+ Surface(
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ ) {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ mode.localized,
+ TextRange(mode.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Accel view mode") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+
+
+ AccelViewMode.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ mode = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ scale.localized,
+ TextRange(scale.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp)
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Accel scale") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ AccelScale.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ scale = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.align(Alignment.End)
+ ) {
+
+ OutlinedButton(
+ onClick = {
+ resultNavigator.navigateBack()
+ }
+ ) {
+
+ Text(
+ text = "Отмена"
+ )
+
+ }
+
+ Button(
+ onClick = {
+ resultNavigator.navigateBack(AccelerometerHistoryFormData(mode, scale))
+ }
+ ) {
+ Text(
+ text = "Сохранить"
+ )
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryContract.kt
new file mode 100644
index 0000000..d97c1c3
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryContract.kt
@@ -0,0 +1,42 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
+
+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.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+
+class AccelerometerHistoryContract {
+
+ sealed class Event : ViewEvent {
+
+ data object StopMeasure : Event()
+
+ data object OnExport : Event()
+
+ data class OnStart(
+ val serial: String,
+ ) : Event()
+
+ data class OnRefreshHistory(
+ val serial: String
+ ) : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data class Display(
+ val bleName: String,
+ val loadingHistoryState : ProgressState>
+ ) : State()
+
+ data object Exception : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryViewModel.kt
new file mode 100644
index 0000000..6b33a52
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AccelerometerHistoryViewModel.kt
@@ -0,0 +1,143 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+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.common.ProgressState
+import llc.arma.ble.domain.usecase.ExportToXlsx
+import llc.arma.ble.domain.usecase.GetAccelerometerHistoryBySerial
+import javax.inject.Inject
+
+@HiltViewModel
+class AccelerometerHistoryViewModel @Inject constructor(
+ private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
+ private val exportToXlsx: ExportToXlsx,
+) : BaseViewModel() {
+
+ private 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.bleName, 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(
+ "event.bleName",
+ ProgressState.Indeterminate
+ )
+ }
+
+ measureJob?.cancel()
+ measureJob = null
+
+ measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
+ it.fold(
+ onSuccess = {
+ setState {
+ AccelerometerHistoryContract.State.Display("event.bleName", 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("event.bleName", 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/AcceleromterHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AcceleromterHistory.kt
similarity index 74%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterHistory.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/history/main/AcceleromterHistory.kt
index d457d20..05f65d6 100644
--- 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/history/main/AcceleromterHistory.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
@@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.CloudUpload
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.TableView
@@ -31,7 +30,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
@@ -50,30 +48,14 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Job
-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.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.screen.locale.localized
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
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.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 java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
-import javax.inject.Inject
class AccelEntry(
val localDate: Long,
@@ -85,43 +67,43 @@ class AccelEntry(
}
+@Destination
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerHistory(
- ble: BleInfo,
- accelScale: AccelScale,
- accelMode: AccelViewMode,
- fftAxis: FftAxis,
- fftMode: FftViewMode,
- frequency: FftFrequency,
- onDismiss: (() -> Unit)? = null,
- onShowStatistic: () -> Unit,
+ bleSerial: String,
+ navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel()
val state = viewModel.viewState.value
- LaunchedEffect(ble.serial) {
- viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
+ LaunchedEffect(bleSerial) {
+ viewModel.setEvent(
+ AccelerometerHistoryContract.Event.OnStart(bleSerial)
+ )
}
Column {
TopAppBar(
navigationIcon = {
- onDismiss?.let {
- IconButton(onClick = it) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = null
- )
+ IconButton(
+ onClick = {
+ navigator.popBackStack()
}
-
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
}
+
+
},
title = {
- val title = when(state){
+ val title = /*when(state){
is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
@@ -130,7 +112,7 @@ fun AccelerometerHistory(
}
}
AccelerometerHistoryContract.State.Exception -> accelMode.localized
- }
+ }*/ ""
Text(
modifier = Modifier.weight(1f),
@@ -141,7 +123,7 @@ fun AccelerometerHistory(
actions = {
IconButton(
- onClick = onShowStatistic,
+ onClick = {} ,
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false
@@ -170,7 +152,9 @@ fun AccelerometerHistory(
IconButton(
onClick = {
- viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
+ viewModel.setEvent(
+ AccelerometerHistoryContract.Event.OnRefreshHistory(bleSerial)
+ )
},
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
@@ -190,8 +174,8 @@ fun AccelerometerHistory(
Box(modifier = Modifier) {
when (state) {
- is AccelerometerHistoryContract.State.Display -> Display(state = state)
- is AccelerometerHistoryContract.State.Exception -> Exception()
+ is AccelerometerHistoryContract.State.Display -> DisplayState(state = state)
+ is AccelerometerHistoryContract.State.Exception -> ErrorState()
}
}
@@ -206,13 +190,14 @@ val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
@Composable
-fun Display(
+private fun DisplayState(
state: AccelerometerHistoryContract.State.Display
) {
- Box(modifier = Modifier
- .padding(8.dp)
- .fillMaxSize()
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxSize()
) {
when (state.loadingHistoryState) {
@@ -514,7 +499,7 @@ fun Display(
}
@Composable
-private fun Exception() {
+private fun ErrorState() {
Box(
modifier = Modifier
.padding(8.dp)
@@ -532,178 +517,5 @@ private fun Exception() {
}
-class AccelerometerHistoryContract {
-
- sealed class Event : ViewEvent {
-
- data object StopMeasure : Event()
-
- data object OnExport : Event()
-
- data class OnStart(
- val bleName: String,
- val serial: String,
- val accelScale: AccelScale,
- val accelMode: AccelViewMode,
- val fftAxis: FftAxis,
- val fftMode: FftViewMode,
- val frequency: FftFrequency
- ) : Event()
-
- data class OnRefreshHistory(
- val bleName: String,
- 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 bleName: String,
- val loadingHistoryState : ProgressState>
- ) : State()
-
- data object Exception : State()
-
- }
-
- sealed class Effect : ViewSideEffect {
-
- }
-
-}
-
-@HiltViewModel
-class AccelerometerHistoryViewModel @Inject constructor(
- private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
- private val exportToXlsx: ExportToXlsx,
-) : BaseViewModel() {
-
- private 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.bleName, 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(event.bleName, ProgressState.Indeterminate)
- }
-
- measureJob?.cancel()
- measureJob = null
-
- measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
- it.fold(
- onSuccess = {
- setState {
- AccelerometerHistoryContract.State.Display(event.bleName, 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(event.bleName, 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/main/AccelerometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerContract.kt
new file mode 100644
index 0000000..cee3aec
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerContract.kt
@@ -0,0 +1,137 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
+
+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.main.view.RealtimeViewMode
+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
+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 {
+
+ data object OnShowChart : Event()
+
+ data object OnShowAccelerometerHistory : Event()
+
+ data object OnShowRealtimeForm : Event()
+
+ data object OnPowerEdit : Event()
+
+ data object OnShowWriteBlePreview : Event()
+
+ data object OnWriteBle : Event()
+
+ data object OnChangePassword : Event()
+
+ data object OnSaveIntervalEdit : Event()
+
+ data object OnReadIntervalEdit : Event()
+
+ data class OnPowerChanged(
+ val tx: BleView.BleState.TX
+ ) : Event()
+
+ data object OnShowHistoryForm : Event()
+
+ data object OnDisableSaveHistory : Event()
+
+ data class OnEnableSaveHistory(
+ val mode: AccelViewMode,
+ val scale: AccelScale
+ ) : Event()
+
+ data class OnSaveIntervalChanged(
+ val interval: Long
+ ) : Event()
+
+ data class OnReadIntervalChanged(
+ val interval: Long
+ ) : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data object Loading : State()
+
+ data class Display(
+ val origin: Ble.Accelerometer,
+ val accelerometer: BleView.Accelerometer,
+ val writeState: WriteState?,
+ val accelViewMode: AccelViewMode,
+ val accelRealtimeViewMode: RealtimeViewMode,
+ 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()
+
+ data object Success : WriteState()
+
+ data object Failure : WriteState()
+
+ }
+
+ }
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+
+ object ShowWriteBle : Effect()
+
+ sealed class Navigation : Effect() {
+
+ data object ShowRealtimeForm : Effect()
+
+ data object ShowHistoryForm : Effect()
+
+ data class SaveIntervalSelector(
+ val interval: Int
+ ) : Navigation()
+
+ data class ReadIntervalSelector(
+ val interval: Int
+ ) : Navigation()
+
+ data class TxPowerSelector(
+ val tx: BleView.BleState.TX
+ ) : Navigation()
+
+ data class ChangePassword(
+ val serial: String
+ ) : Navigation()
+
+ data class AccelHistory(
+ val ble: BleInfo,
+ val accelScale: AccelScale,
+ val accelMode: AccelViewMode,
+ val fftAxis: FftAxis,
+ val fftMode: FftViewMode,
+ val frequency: FftFrequency
+ ) : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerScreen.kt
new file mode 100644
index 0000000..59019ba
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerScreen.kt
@@ -0,0 +1,221 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material3.Scaffold
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+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.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryDestination
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryFormDestination
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeFormDestination
+import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultRecipient
+import com.ramcosta.composedestinations.result.onResult
+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.rememberBottomDialogState
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form.AccelerometerHistoryFormData
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.DisplayState
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.LoadingState
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.Write
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleInfo
+
+enum class SheetPage {
+ WRITE
+}
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AccelerometerScreen(
+ navigator: DestinationsNavigator,
+ bleSerial: String,
+ historyFormResult: ResultRecipient,
+ txSelectResult: ResultRecipient,
+ readDurationSelectResult: ResultRecipient,
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ val bottomDialog = rememberBottomDialogState()
+
+ var sheetPage by rememberSaveable {
+ mutableStateOf(null)
+ }
+
+ historyFormResult.onResult {
+ viewModel.setEvent(AccelerometerContract.Event.OnEnableSaveHistory(it.mode, it.scale))
+ }
+
+ txSelectResult.onResult {
+ viewModel.setEvent(AccelerometerContract.Event.OnPowerChanged(it))
+ }
+
+ readDurationSelectResult.onResult {
+
+ if(it.qualifier == "ReadIntervalSelector"){
+ viewModel.setEvent(AccelerometerContract.Event.OnReadIntervalChanged(it.duration.toLong()))
+ }
+
+ if(it.qualifier == "SaveIntervalSelector"){
+ viewModel.setEvent(AccelerometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
+ }
+
+ }
+
+ LaunchedEffect(
+ key1 = bottomDialog.sheetState?.currentValue,
+ block = {
+ if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
+ bottomDialog.setContent {}
+ sheetPage = null
+ }
+ }
+ )
+
+ val scope = rememberCoroutineScope()
+
+ LaunchedEffect(sheetPage) {
+ when (sheetPage) {
+ SheetPage.WRITE -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if (currentState is AccelerometerContract.State.Display) {
+
+ currentState.writeState?.let {
+
+ Write(
+ state = it,
+ onEvent = viewModel::setEvent
+ )
+
+ }
+
+ }
+ }
+ null -> {
+ bottomDialog.hide()
+ }
+
+ }
+
+ }
+
+ DisposableEffect(Unit){
+ onDispose {
+ scope.launch {
+ bottomDialog.hide()
+ }
+ }
+ }
+
+ LaunchedEffect("effect"){
+ viewModel.effect.onEach {
+ when(it){
+
+ is AccelerometerContract.Effect.ShowWriteBle -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
+
+ is AccelerometerContract.Effect.Navigation.AccelHistory ->
+ navigator.navigate(AccelerometerHistoryDestination(it.ble.serial))
+
+ is AccelerometerContract.Effect.Navigation.ChangePassword ->
+ navigator.navigate(ChangePasswordScreenDestination(it.serial))
+
+ is AccelerometerContract.Effect.Navigation.ReadIntervalSelector ->
+ navigator.navigate(DurationSelectorScreenDestination(
+ qualifier = "ReadIntervalSelector",
+ duration = it.interval
+ ))
+
+ is AccelerometerContract.Effect.Navigation.SaveIntervalSelector ->
+ navigator.navigate(DurationSelectorScreenDestination(
+ qualifier = "SaveIntervalSelector",
+ duration = it.interval
+ ))
+
+ is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
+ navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
+
+ AccelerometerContract.Effect.Navigation.ShowHistoryForm ->
+ navigator.navigate(AccelerometerHistoryFormDestination())
+
+ AccelerometerContract.Effect.Navigation.ShowRealtimeForm ->
+ navigator.navigate(AccelerometerRealtimeFormDestination(bleSerial))
+ }
+ }.launchIn(this)
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ navigator.popBackStack()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text(text = BleInfo.Type.ACCELEROMETER.localized)
+ }
+ )
+ }
+ ) {
+
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when(state){
+ is AccelerometerContract.State.Display -> {
+ DisplayState(
+ origin = state.origin,
+ ble = state.accelerometer,
+ onEvent = viewModel::setEvent
+ )
+ }
+ is AccelerometerContract.State.Loading -> LoadingState()
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerViewModel.kt
new file mode 100644
index 0000000..f17b578
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/AccelerometerViewModel.kt
@@ -0,0 +1,389 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
+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.accelerometer.main.view.RealtimeViewMode
+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.GetBleBySerial
+import llc.arma.ble.domain.usecase.WriteBle
+import javax.inject.Inject
+
+@HiltViewModel
+class AccelerometerViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ getBleBySerial: GetBleBySerial,
+ private val bleMapper: BleMapper,
+ private val bleViewMapper: BleViewMapper,
+ private val writeBle: WriteBle
+) : BaseViewModel() {
+
+ init {
+
+ val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
+
+ viewModelScope.launch {
+
+ val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
+ onSuccess = { it },
+ onFailure = { null }
+ )
+
+ if(ble != null && ble is Ble.Accelerometer){
+ setState {
+
+ when(this){
+ is AccelerometerContract.State.Display -> {
+ copy(
+ origin = Ble.Accelerometer(
+ info = ble.info,
+ state = origin.state,
+ accelerometerState = origin.accelerometerState
+ )
+ )
+ }
+ AccelerometerContract.State.Loading -> {
+ AccelerometerContract.State.Display(
+ origin = ble,
+ accelerometer = bleMapper.map(ble) as BleView.Accelerometer,
+ writeState = null,
+ accelViewMode = AccelViewMode.ACCELERATION,
+ accelRealtimeViewMode = RealtimeViewMode.Accel(
+ AccelViewMode.ACCELERATION
+ ),
+ fftAxis = FftAxis.AUTO,
+ fftFrequency = FftFrequency.F_400,
+ fftViewMode = FftViewMode.SPECTRE,
+ accelScale = AccelScale.S_2
+ )
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
+ override fun setInitialState() = AccelerometerContract.State.Loading
+
+ override fun handleEvents(event: AccelerometerContract.Event) {
+ when(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.OnWriteBle -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnDisableSaveHistory -> 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.OnReadIntervalChanged -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnEnableSaveHistory -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnShowHistoryForm -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnShowChart -> reduce(viewState.value, event)
+ is AccelerometerContract.Event.OnShowRealtimeForm -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnShowChart
+ ) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.ShowHistoryForm
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnShowRealtimeForm
+ ) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.ShowRealtimeForm
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnShowHistoryForm
+ ) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.ShowHistoryForm
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnReadIntervalChanged
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ state.accelerometer.accelerometerState.readInterval = event.interval
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnReadIntervalEdit
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.ReadIntervalSelector(
+ state.accelerometer.accelerometerState.readInterval.toInt()
+ )
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnSaveIntervalEdit
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.SaveIntervalSelector(
+ state.accelerometer.accelerometerState.historyInterval.toInt()
+ )
+ }
+
+ }
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnSaveIntervalChanged
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ state.accelerometer.accelerometerState.historyInterval = event.interval
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnChangePassword
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.ChangePassword(state.accelerometer.info.serial)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnDisableSaveHistory
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Disabled
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnEnableSaveHistory
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Enabled(
+ scale = event.scale,
+ mode = event.mode,
+ detailed = true
+ )
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnShowAccelerometerHistory
+ ) {
+
+ if (state is AccelerometerContract.State.Display &&
+ state.origin.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled
+ ) {
+ setEffect {
+
+ AccelerometerContract.Effect.Navigation.AccelHistory(
+ ble = state.accelerometer.info,
+ accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
+ fftAxis = state.fftAxis,
+ fftMode = state.fftViewMode,
+ frequency = state.fftFrequency,
+ accelScale = state.accelScale
+ )
+ }
+
+ }
+
+ }
+
+ 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,
+ saveHistorySettings = if(newBle.accelerometerState.saveHistorySettings == state.origin.accelerometerState.saveHistorySettings) null else newBle.accelerometerState.saveHistorySettings,
+ historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
+ readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval,
+ )
+
+ 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
+
+ }
+
+ }
+
+ private fun reduce(
+ state: AccelerometerContract.State,
+ event: AccelerometerContract.Event.OnPowerEdit
+ ) {
+
+ if(state is AccelerometerContract.State.Display) {
+
+ setEffect {
+ AccelerometerContract.Effect.Navigation.TxPowerSelector(state.accelerometer.state.tx)
+ }
+
+ }
+ }
+
+ 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(
+ saveHistorySettings = request.writeRequest.saveHistorySettings
+ ?: currentState.origin.accelerometerState.saveHistorySettings
+ )
+ )
+
+ 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/AccelFftAxisEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/AccelFftAxisEdit.kt
similarity index 56%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftAxisEdit.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/AccelFftAxisEdit.kt
index cf2495f..811870b 100644
--- 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/main/view/AccelFftAxisEdit.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftAxis
@@ -44,45 +44,4 @@ fun SelectorItem(
Text(text = label)
}
-}
-
-@Composable
-fun AccelFftAxisEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Fft axis",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- FftAxis.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = it == state.fftAxis
- ){
- onEvent(AccelerometerContract.Event.OnFftAxisChanged(it))
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- }
-
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/AccelRealtimeViewEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/AccelRealtimeViewEdit.kt
new file mode 100644
index 0000000..3792582
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/AccelRealtimeViewEdit.kt
@@ -0,0 +1,49 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import kotlinx.serialization.Serializable
+import llc.arma.ble.app.ui.common.PrimaryButton
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.usecase.AccelViewMode
+
+@Serializable
+sealed class RealtimeViewMode {
+
+ companion object {
+
+ val entries: List
+ get() {
+ return AccelViewMode.entries.map {
+ Accel(it)
+ } + Spectre
+ }
+
+ }
+
+ data class Accel(
+ val accelViewMode: AccelViewMode
+ ): RealtimeViewMode()
+
+ data object Spectre : RealtimeViewMode()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/DisplayState.kt
new file mode 100644
index 0000000..e8ef350
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/DisplayState.kt
@@ -0,0 +1,201 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.common.PrimaryButton
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.BleInfoView
+import llc.arma.ble.app.ui.screen.ShapeType
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.data.repository.BleRepositoryImpl
+import llc.arma.ble.domain.model.Ble
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@Composable
+fun DisplayState(
+ onEvent: (AccelerometerContract.Event) -> Unit,
+ origin: Ble.Accelerometer,
+ ble: BleView.Accelerometer
+) {
+
+ val scrollState = rememberScrollState()
+
+ Column {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .verticalScroll(scrollState)
+ .weight(1f)
+ ) {
+
+ BleInfoView(
+ bleInfo = origin.info,
+ version = origin.state.version
+ )
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+
+ BleMenuItem(
+ shapeType = ShapeType.Start,
+ title = "Мощность",
+ subtitle = "${ble.state.tx.value} db",
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+ onEvent(AccelerometerContract.Event.OnPowerEdit)
+ }
+
+ val history = ble.accelerometerState.saveHistory
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Сохранять измерения",
+ subtitle = if (history is Ble.Accelerometer.HistorySettings.Enabled) {
+ "View mode ${history.mode.localized} Scale: ${history.scale.localized}"
+ } else {
+ ""
+ },
+ icon = {
+ Switch(
+ checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled,
+ onCheckedChange = {
+ if(it){
+ onEvent(AccelerometerContract.Event.OnShowHistoryForm)
+ } else {
+ onEvent(AccelerometerContract.Event.OnDisableSaveHistory)
+ }
+
+ }
+ )
+ }
+ )
+
+ if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Интервал измерений",
+ subtitle = ble.accelerometerState.historyInterval
+ .toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
+ "$hours ч. $minutes мин. $seconds сек." },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+
+ onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
+
+ }
+
+ }
+
+ if (ble.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
+
+ if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Интервал чтения",
+ subtitle = ble.accelerometerState.readInterval
+ .toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
+ "$hours ч. $minutes мин. $seconds сек." },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+
+ onEvent(AccelerometerContract.Event.OnReadIntervalEdit)
+
+ }
+
+ }
+
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "График измерений",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+ ) {
+
+ when (origin.accelerometerState.saveHistorySettings) {
+ is Ble.Accelerometer.HistorySettings.Disabled ->
+ onEvent(AccelerometerContract.Event.OnShowRealtimeForm)
+
+ is Ble.Accelerometer.HistorySettings.Enabled ->
+ onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
+ }
+
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.End,
+ title = "Изменить пароль",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+ ) {
+ onEvent(AccelerometerContract.Event.OnChangePassword)
+ }
+
+ }
+
+ }
+
+ PrimaryButton(
+ modifier = Modifier.shadow(
+ if(scrollState.canScrollForward){
+ 8.dp
+ } else {
+ 0.dp
+ }
+ ).background(MaterialTheme.colorScheme.background),
+ label = "Сохранить"
+ ) {
+
+ onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/LoadingState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/LoadingState.kt
new file mode 100644
index 0000000..944f8f1
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/LoadingState.kt
@@ -0,0 +1,25 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun LoadingState(){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ ContainedLoadingIndicator()
+
+ }
+
+}
\ 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/main/view/Write.kt
similarity index 87%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/Write.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/main/view/Write.kt
index 2a286ee..5f83865 100644
--- 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/main/view/Write.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@@ -21,8 +21,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
+import llc.arma.ble.app.ui.screen.ShapeType
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
+import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localizedName
import llc.arma.ble.domain.model.Ble
@@ -57,6 +61,7 @@ fun Write(
state.writeRequest.tx?.let {
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db",
)
@@ -66,6 +71,7 @@ fun Write(
state.writeRequest.saveHistorySettings?.let {
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
@@ -82,6 +88,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
@@ -95,6 +102,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
@@ -112,7 +120,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
- onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
+ //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
} else {
@@ -130,7 +138,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
+ //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@@ -155,7 +163,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
- onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
+ //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@@ -197,7 +205,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
+ //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@@ -239,7 +247,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
+ //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelContract.kt
new file mode 100644
index 0000000..4fcb244
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelContract.kt
@@ -0,0 +1,36 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
+
+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.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 AccelerometerAccelContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnRefresh : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data class Display(
+ val mode: AccelViewMode,
+ val measureHistory : List
+ ) : State()
+
+ data object Exception : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelViewModel.kt
new file mode 100644
index 0000000..10cf6e0
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerAccelViewModel.kt
@@ -0,0 +1,114 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+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.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.GetAccelerometerMeasureBySerialFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class AccelerometerAccelViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
+) : BaseViewModel() {
+
+ private var measureJob: Job? = null
+
+ init {
+ startReadMeasure(false)
+ }
+
+ override fun setInitialState() = AccelerometerAccelContract.State.Display(
+ mode = AccelViewMode.ACCELERATION,
+ measureHistory = emptyList()
+ )
+
+ override fun handleEvents(event: AccelerometerAccelContract.Event) {
+ when(event){
+ is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: AccelerometerAccelContract.State,
+ event: AccelerometerAccelContract.Event.OnRefresh
+ ) {
+ startReadMeasure(true)
+ }
+
+ private fun startReadMeasure(
+ restartJob: Boolean
+ ){
+
+ val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle)
+
+ if(restartJob || measureJob == null) {
+ measureJob?.cancel()
+ measureJob = null
+ measureJob = viewModelScope.launch {
+
+ setState {
+ AccelerometerAccelContract.State.Display(
+ mode = AccelViewMode.ACCELERATION,
+ measureHistory = emptyList()
+ )
+ }
+
+ getAccelerometerMeasureBySerialFlow(
+ params.bleSerial,
+ params.accelScale,
+ params.accelMode,
+ params.fftAxis,
+ params.fftMode,
+ params.frequency
+ ).onEach {
+ it.fold(
+ onSuccess = {
+ setState {
+ when (this) {
+ is AccelerometerAccelContract.State.Display -> {
+ var dataList = this.measureHistory.toMutableList().apply {
+ add(it)
+ }
+ if(params.accelMode != AccelViewMode.ANGLE) {
+ dataList = dataList.takeLast(100).toMutableList()
+ }
+ AccelerometerAccelContract.State.Display(
+ params.accelMode,
+ dataList
+ )
+ }
+
+ AccelerometerAccelContract.State.Exception -> {
+ AccelerometerAccelContract.State.Display(
+ params.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/rt/AccelerometerRealtimeForm.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerRealtimeForm.kt
new file mode 100644
index 0000000..ce2132b
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AccelerometerRealtimeForm.kt
@@ -0,0 +1,443 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.input.TextFieldLineLimits
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.Sort
+import androidx.compose.material3.Button
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuAnchorType
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.unit.dp
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultBackNavigator
+import com.ramcosta.composedestinations.spec.DestinationStyle
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.filter.BleFilterContract
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleFilter
+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
+
+@Serializable
+data class AccelerometerRealtimeFormData(
+ val mode: RealtimeViewMode,
+ val fftViewMode: FftViewMode,
+ val fftAxis: FftAxis,
+ val fftFrequency: FftFrequency,
+ val scale: AccelScale
+)
+
+@Destination(style = DestinationStyle.Dialog::class)
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AccelerometerRealtimeForm(
+ navigator: DestinationsNavigator,
+ bleSerial: String,
+) {
+
+ var mode by remember { mutableStateOf(RealtimeViewMode.Accel(AccelViewMode.ACCELERATION)) }
+ var fftMode by remember { mutableStateOf(FftViewMode.entries.first()) }
+ var fftAxis by remember { mutableStateOf(FftAxis.entries.first()) }
+ var fftFrequency by remember { mutableStateOf(FftFrequency.entries.first()) }
+ var scale by remember { mutableStateOf(AccelScale.entries.first()) }
+
+ Surface(
+ shape = RoundedCornerShape(20.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ ) {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier.padding(20.dp)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ mode.localized,
+ TextRange(mode.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Измеряемый параметр") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ RealtimeViewMode.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ mode = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ if(mode is RealtimeViewMode.Spectre) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ fftMode.localized,
+ TextRange(fftMode.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Режим просмотра") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ FftViewMode.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ fftMode = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ fftAxis.localized,
+ TextRange(fftAxis.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Ось") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ FftAxis.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ fftAxis = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ fftFrequency.localized,
+ TextRange(fftFrequency.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp),
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Частота дискретизации") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ FftFrequency.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ fftFrequency = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+
+ var expanded by remember { mutableStateOf(false) }
+
+ val sortTextFileState = TextFieldState(
+ scale.localized,
+ TextRange(scale.localized.length)
+ )
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = it
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 8.dp)
+ ) {
+
+ OutlinedTextField(
+ state = sortTextFileState,
+ readOnly = true,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ label = { Text("Масштаб") },
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(
+ expanded = expanded
+ )
+ },
+ modifier = Modifier
+ .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
+ .fillMaxWidth(),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+
+ AccelScale.entries.forEach { selectionOption ->
+ DropdownMenuItem(
+ onClick = {
+ scale = selectionOption
+ expanded = false
+ },
+ text = {
+ Text(text = selectionOption.localized)
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
+ )
+ }
+ }
+ }
+
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.align(Alignment.End)
+ ) {
+
+ OutlinedButton(
+ onClick = {
+ navigator.popBackStack()
+ }
+ ) {
+
+ Text(
+ text = "Отмена"
+ )
+
+ }
+
+ Button(
+ onClick = {
+ when (mode) {
+ is RealtimeViewMode.Accel -> {
+
+ navigator.navigate(AccelerometerRealtimeDestination(
+ bleSerial = bleSerial,
+ accelMode = (mode as RealtimeViewMode.Accel).accelViewMode,
+ fftAxis = fftAxis,
+ fftMode = fftMode,
+ frequency = fftFrequency,
+ accelScale = scale
+ ))
+
+ }
+
+ is RealtimeViewMode.Spectre -> {
+
+ navigator.navigate(AccelerometerSpectreDestination(
+ bleSerial = bleSerial,
+ accelMode = AccelViewMode.ACCELERATION,
+ fftAxis = fftAxis,
+ fftMode = fftMode,
+ frequency = fftFrequency,
+ accelScale = scale
+ ))
+
+ }
+ }
+ }
+ ) {
+ Text(
+ text = "Продолжить"
+ )
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ 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/rt/AcceleromterAccel.kt
similarity index 71%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AcceleromterAccel.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/rt/AcceleromterAccel.kt
index 18a067f..851ca0e 100644
--- 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/rt/AcceleromterAccel.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
@@ -15,15 +15,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -32,7 +34,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
@@ -47,110 +48,85 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Job
-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.app.ui.common.ViewEvent
-import llc.arma.ble.app.ui.common.ViewSideEffect
-import llc.arma.ble.app.ui.common.ViewState
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized
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
-import llc.arma.ble.domain.usecase.AccelViewMode.ACCELERATION
-import llc.arma.ble.domain.usecase.AccelViewMode.ANGLE
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
-import javax.inject.Inject
+@OptIn(ExperimentalMaterial3Api::class)
+@Destination
@Composable
fun AccelerometerRealtime(
- ble: BleInfo,
+ bleSerial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
- onDismiss: (() -> Unit)? = null
+ navigator: DestinationsNavigator
) {
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
- ) {
-
- onDismiss?.let {
-
- IconButton(onClick = it) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = null
- )
- }
-
- }
-
- 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))
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = navigator::popBackStack
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
},
- enabled = true
- ) {
- Icon(
- imageVector = Icons.Rounded.Refresh,
- contentDescription = null
- )
- }
+ title = {
+ Text(
+ text = accelMode.localized,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
+ },
+ enabled = true
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = null
+ )
+ }
+ }
+ )
}
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Box(modifier = Modifier) {
+ ) {
+ Box(modifier = Modifier.padding(it)) {
when (state) {
- is AccelerometerAccelContract.State.Display -> Display(state = state)
- is AccelerometerAccelContract.State.Exception -> Exception()
+ is AccelerometerAccelContract.State.Display -> DisplayState(state = state)
+ is AccelerometerAccelContract.State.Exception -> ExceptionState()
}
}
-
}
}
@Composable
-fun Display(
+private fun DisplayState(
state: AccelerometerAccelContract.State.Display
) {
@@ -528,7 +504,7 @@ fun Angle(
}
@Composable
-private fun Exception(
+private fun ExceptionState(
) {
Box(
@@ -546,161 +522,4 @@ private fun Exception(
}
-}
-
-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() {
-
- private var measureJob: Job? = null
-
- private var lastSerial: String? = null
-
- override fun setInitialState() = AccelerometerAccelContract.State.Display(
- mode = 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 = 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 = 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 != ANGLE) {
- dataList = dataList.takeLast(100).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/spectre/AccelerometerSpectre.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectre.kt
new file mode 100644
index 0000000..5cc1ca6
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectre.kt
@@ -0,0 +1,336 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Refresh
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
+import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
+import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
+import com.patrykandpatrick.vico.compose.chart.Chart
+import com.patrykandpatrick.vico.compose.chart.column.columnChart
+import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
+import com.patrykandpatrick.vico.compose.component.textComponent
+import com.patrykandpatrick.vico.core.axis.AxisPosition
+import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
+import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
+import com.patrykandpatrick.vico.core.entry.ChartEntry
+import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
+import com.patrykandpatrick.vico.core.entry.FloatEntry
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.BleInfo
+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 AccelerometerEntry(
+ val frequency: Long,
+ override val x: Float,
+ override val y: Float,
+) : ChartEntry {
+
+ override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Destination
+@Composable
+fun AccelerometerSpectre(
+ bleSerial: String,
+ accelMode: AccelViewMode,
+ fftAxis: FftAxis,
+ fftMode: FftViewMode,
+ frequency: FftFrequency,
+ accelScale: AccelScale,
+ navigator: DestinationsNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ navigator.popBackStack()
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Row(
+ 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
+ }
+
+ 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 -> {}
+ }
+
+ }
+
+ }
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(AccelerometerSpectreContract.Event.OnRefresh)
+ },
+ enabled = when(state){
+ is AccelerometerSpectreContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
+ AccelerometerSpectreContract.State.Exception -> true
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = null
+ )
+ }
+ }
+ )
+ }
+ ) {
+
+ Box(
+ modifier = Modifier
+ .padding(it)
+ ) {
+
+ when (state) {
+ is AccelerometerSpectreContract.State.Display -> DisplayState(state = state)
+ AccelerometerSpectreContract.State.Exception -> ExceptionState()
+ }
+
+ }
+
+ }
+
+}
+
+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 DisplayState(
+ 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,
+ persistentMarkers = mapOf(producer.getModel().maxX to MarkerComponent(
+ label = textComponent(),
+ indicator = null,
+ guideline = axisGuidelineComponent()
+ )),
+ )
+
+ val scrollState = rememberChartScrollState()
+
+ val marker = MarkerComponent(
+ label = textComponent(),
+ indicator = null,
+ guideline = axisGuidelineComponent()
+ )
+
+ Chart(
+ marker = marker,
+ 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 ExceptionState(
+
+) {
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ .aspectRatio(2f),
+ ){
+
+ Text(
+ textAlign = TextAlign.Center,
+ text = "Во время загрузки произошла ошибка",
+ modifier = Modifier.align(Alignment.Center)
+ )
+
+ }
+
+}
+
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreContract.kt
new file mode 100644
index 0000000..7c5b7d4
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreContract.kt
@@ -0,0 +1,32 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
+
+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.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+
+class AccelerometerSpectreContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnRefresh : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data class Display(
+ val previousHistory : List?,
+ val loadingHistoryState : ProgressState>
+ ) : State()
+
+ data object Exception : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreViewModel.kt
new file mode 100644
index 0000000..fd90c65
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/spectre/AccelerometerSpectreViewModel.kt
@@ -0,0 +1,128 @@
+package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+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.common.ProgressState
+import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
+import javax.inject.Inject
+
+@HiltViewModel
+class AccelerometerSpectreViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
+) : BaseViewModel() {
+
+ private var job: Job? = null
+ private var lastSerial: String? = null
+
+ init {
+
+ loadData()
+
+ }
+
+ override fun setInitialState() = AccelerometerSpectreContract.State.Display(
+ loadingHistoryState = ProgressState.Indeterminate,
+ previousHistory = null
+ )
+
+ override fun handleEvents(event: AccelerometerSpectreContract.Event) {
+ when(event){
+ is AccelerometerSpectreContract.Event.OnRefresh -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: AccelerometerSpectreContract.State,
+ event: AccelerometerSpectreContract.Event.OnRefresh
+ ) {
+ loadData()
+ }
+
+ private fun loadData(){
+
+ val params = AccelerometerSpectreDestination.argsFrom(savedStateHandle)
+ val state = viewState.value
+
+ viewModelScope.launch {
+
+ if(state is AccelerometerSpectreContract.State.Display) {
+
+ lastSerial = params.bleSerial
+
+ 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 = params.bleSerial,
+ accelMode = params.accelMode,
+ fftAxis = params.fftAxis,
+ fftMode = params.fftMode,
+ frequency = params.frequency,
+ accelScale = params.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)
+
+ }
+
+ }
+
+}
\ 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
deleted file mode 100644
index 6116f48..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelEdit.kt
+++ /dev/null
@@ -1,272 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-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.Text
-import androidx.compose.runtime.Composable
-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.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-
-@Composable
-fun AccelEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- val accelMode = state.accelRealtimeViewMode
- 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.OnRealtimeViewModeEdit)
- }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Измеряемый параметр"
- )
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = accelMode.localized
- )
-
- }
-
- Icon(
- imageVector = Icons.Rounded.KeyboardArrowDown,
- contentDescription = null
- )
-
- }
-
- }
-
- if(accelMode is RealtimeViewMode.Spectre){
-
- 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 = "Режим просмотра"
- )
- 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 = "Ось"
- )
- 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 = "Частота дистретизации"
- )
- 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.ACCEL))
- }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Масштаб"
- )
- 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))
-
- PrimaryButton(
- label = "Продолжить"
- ) {
- onEvent(AccelerometerContract.Event.OnShowAccelerometerAccel)
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index 51c2989..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFftModeEdit.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.usecase.FftViewMode
-
-@Composable
-fun AccelFftModeEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- 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.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = it == state.fftViewMode
- ){
- onEvent(AccelerometerContract.Event.OnFftModeChanged(it))
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index 5c15d84..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelFrequencyEdit.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.usecase.FftFrequency
-
-@Composable
-fun AccelFrequencyEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Fft frequency",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- FftFrequency.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = it == state.fftFrequency
- ){
- onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it))
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelRealtimeViewEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelRealtimeViewEdit.kt
deleted file mode 100644
index 62ef2ac..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelRealtimeViewEdit.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.RadioButton
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.usecase.AccelViewMode
-
-sealed class RealtimeViewMode {
-
- data class Accel(
- val accelViewMode: AccelViewMode
- ): RealtimeViewMode()
-
- object Spectre : RealtimeViewMode()
-
-}
-
-@Composable
-fun AccelRealtimeViewEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- var value by remember(state.accelRealtimeViewMode) {
- mutableStateOf(state.accelRealtimeViewMode)
- }
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Accel view mode",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- AccelViewMode.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = value is RealtimeViewMode.Accel && it == (value as RealtimeViewMode.Accel).accelViewMode
- ){
- value = RealtimeViewMode.Accel(it)
- }
-
- }
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
- .clickable { value = RealtimeViewMode.Spectre }
- .padding(4.dp)
- ) {
-
- RadioButton(
- selected = value is RealtimeViewMode.Spectre,
- onClick = {
- value = RealtimeViewMode.Spectre
- }
- )
-
- Text(text = RealtimeViewMode.Spectre.localized)
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
- onEvent(AccelerometerContract.Event.OnRealtimeViewModeChanged(value))
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index 1fb5c79..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelScaleEdit.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.model.Ble
-import llc.arma.ble.domain.usecase.AccelScale
-
-@Composable
-fun AccelScaleEdit(
- next: AccelerometerContract.Event.Next,
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- val fftMode = when(next){
- AccelerometerContract.Event.Next.ACCEL ->
- state.accelScale
- AccelerometerContract.Event.Next.HISTORY -> {
- val history = state.accelerometer.accelerometerState.saveHistory
- if (history is Ble.Accelerometer.HistorySettings.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.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = it == fftMode
- ){
- when(next){
- AccelerometerContract.Event.Next.ACCEL ->
- onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
- AccelerometerContract.Event.Next.HISTORY ->
- onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
- }
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
- when(next){
- AccelerometerContract.Event.Next.ACCEL ->
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- AccelerometerContract.Event.Next.HISTORY ->
- onEvent(AccelerometerContract.Event.OnHistoryEdit)
- }
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index 110392d..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelViewEdit.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.usecase.AccelViewMode
-
-@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))
-
- AccelViewMode.entries.forEach {
-
- SelectorItem(
- label = it.localized,
- selected = it == value
- ){
- value = it
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Ок"
- ) {
-
- when(next){
- AccelerometerContract.Event.Next.ACCEL -> {
- onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- }
- AccelerometerContract.Event.Next.HISTORY -> {
- onEvent(AccelerometerContract.Event.OnHistoryViewModeChanged(value))
- onEvent(AccelerometerContract.Event.OnHistoryEdit)
- }
- }
-
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index 70da075..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/AccelerometerSpectre.kt
+++ /dev/null
@@ -1,540 +0,0 @@
-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.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.viewModelScope
-import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
-import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
-import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
-import com.patrykandpatrick.vico.compose.chart.Chart
-import com.patrykandpatrick.vico.compose.chart.column.columnChart
-import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
-import com.patrykandpatrick.vico.compose.component.textComponent
-import com.patrykandpatrick.vico.core.axis.AxisPosition
-import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
-import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
-import com.patrykandpatrick.vico.core.entry.ChartEntry
-import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
-import com.patrykandpatrick.vico.core.entry.FloatEntry
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Job
-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.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.screen.locale.localized
-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.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
-import javax.inject.Inject
-
-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,
- onDismiss: (() -> Unit)? = null
-) {
-
- 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 {
- viewModel.setEvent(AccelerometerSpectreContract.Event.StopMeasure)
- }
-
- })
-
- Column(
- modifier = Modifier.fillMaxHeight(0.9f)
- ) {
-
- Row(
- modifier = Modifier.padding(horizontal = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- onDismiss?.let {
-
- IconButton(onClick = it) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = null
- )
- }
-
- }
-
- 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,
- persistentMarkers = mapOf(producer.getModel().maxX to MarkerComponent(
- label = textComponent(),
- indicator = null,
- guideline = axisGuidelineComponent()
- )),
- )
-
- val scrollState = rememberChartScrollState()
-
- val marker = MarkerComponent(
- label = textComponent(),
- indicator = null,
- guideline = axisGuidelineComponent()
- )
-
- Chart(
- marker = marker,
- 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()
-
- data 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) {
-
- 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/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt
deleted file mode 100644
index 1b4ace9..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/DisplayState.kt
+++ /dev/null
@@ -1,218 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowRight
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-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.host.view.BleMenuItem
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.data.repository.BleRepositoryImpl
-import llc.arma.ble.domain.model.Ble
-
-@Composable
-fun DisplayState(
- onEvent: (AccelerometerContract.Event) -> Unit,
- origin: Ble.Accelerometer,
- ble: BleView.Accelerometer
-) {
-
- val scrollState = rememberScrollState()
-
- Column {
-
- Column(
- modifier = Modifier
- .verticalScroll(scrollState)
- .weight(1f)
- ) {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
- BleInfoView(
- bleInfo = origin.info,
- version = origin.state.version
- )
- }
-
- Column(
- modifier = Modifier,
- content = {
-
- BleMenuItem(
- title = "Мощность",
- subtitle = "${ble.state.tx.value} db",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ){
- onEvent(AccelerometerContract.Event.OnPowerEdit)
- }
-
- 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.HistorySettings.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.HistorySettings.Enabled,
- onCheckedChange = {
- onEvent(AccelerometerContract.Event.OnSaveHistoryChanged(it))
- }
- )
-
- }
-
- }
-
- if(ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
-
- val hours =
- ble.accelerometerState.historyInterval / millisInHour
- val minutes =
- (ble.accelerometerState.historyInterval - (hours * millisInHour)) / millisInMinute
- val seconds =
- (ble.accelerometerState.historyInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
-
- BleMenuItem(
- title = "Интервал измерений",
- subtitle = "$hours ч. $minutes мин. $seconds сек.",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
-
- onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
-
- }
-
- }
-
- if(ble.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
-
- if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
-
- val hours =
- ble.accelerometerState.readInterval / millisInHour
- val minutes =
- (ble.accelerometerState.readInterval - (hours * millisInHour)) / millisInMinute
- val seconds =
- (ble.accelerometerState.readInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
-
- BleMenuItem(
- title = "Интервал чтения",
- subtitle = "$hours ч. $minutes мин. $seconds сек.",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
-
- onEvent(AccelerometerContract.Event.OnReadIntervalEdit)
-
- }
-
- }
-
- }
-
- BleMenuItem(
- title = "График измерений",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
-
- when(origin.accelerometerState.saveHistorySettings){
- is Ble.Accelerometer.HistorySettings.Disabled ->
- onEvent(AccelerometerContract.Event.OnAccelEdit)
- is Ble.Accelerometer.HistorySettings.Enabled ->
- onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
- }
-
- }
-
- BleMenuItem(
- title = "Изменить пароль",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
- onEvent(AccelerometerContract.Event.OnChangePassword)
- }
-
- }
- )
-
- }
-
- PrimaryButton(
- modifier = Modifier.shadow(
- if(scrollState.canScrollForward){
- 8.dp
- } else {
- 0.dp
- }
- ).background(MaterialTheme.colorScheme.background),
- label = "Сохранить"
- ) {
-
- onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
-
- }
-
- }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index 1945faa..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/HistoryEdit.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-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.Text
-import androidx.compose.runtime.Composable
-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.common.PrimaryButton
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-import llc.arma.ble.app.ui.screen.locale.localized
-import llc.arma.ble.domain.model.Ble
-
-@Composable
-fun HistoryEdit(
- state: AccelerometerContract.State.Display,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- val history = state.accelerometer.accelerometerState.saveHistory
-
- val detailed = if (history is Ble.Accelerometer.HistorySettings.Enabled) history.detailed else false
- val accelMode = if (history is Ble.Accelerometer.HistorySettings.Enabled) history.mode else state.accelViewMode
- val accelScale = if (history is Ble.Accelerometer.HistorySettings.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))
-
- PrimaryButton(
- label = "Ок"
- ) {
- onEvent(AccelerometerContract.Event.OnHideHistoryEdit)
- }
-
- }
-
-}
\ 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
deleted file mode 100644
index ba4838d..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/IntervalEdit.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowUp
-import androidx.compose.material3.FilledIconButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
-
-@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
- val minInterval = 1_000
-
- if(value > maxInterval){
- value = maxInterval
- }
-
- if(value < minInterval){
- value = minInterval
- }
-
- val maxSeconds = maxInterval / millisInSecond
- 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
- val secondsValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour) - (minutesValue * millisInMinute)) / millisInSecond
-
- 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) + (secondsValue * millisInSecond)
- }
- )
-
- 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) + (secondsValue * millisInSecond)
- }
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Text(text = "Ч.")
-
- Spacer(modifier = Modifier.width(16.dp))
-
- NumberPicker(
- range = -1..maxMinutes,
- value = minutesValue,
- onValueChanged = {
- value = (secondsValue * millisInSecond) + (it * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour)
- }
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Text(text = "М.")
-
- Spacer(modifier = Modifier.width(16.dp))
-
- NumberPicker(
- range = -1..maxSeconds,
- value = secondsValue,
- onValueChanged = {
- value = (it * millisInSecond) + (minutesValue * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour)
- }
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Text(text = "С.")
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(
- AccelerometerContract.Event.OnSaveIntervalChanged(
- value.toLong()
- )
- )
- }
-
- }
-
-}
-
-const val millisInSecond = 1000
-const val millisInMinute = millisInSecond * 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/LoadingState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/LoadingState.kt
deleted file mode 100644
index c2906be..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/LoadingState.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-
-@Composable
-fun LoadingState(){
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/ReadIntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/ReadIntervalEdit.kt
deleted file mode 100644
index 13ab0a7..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/accelerometer/view/ReadIntervalEdit.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-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.host.view.DurationPicker
-
-@Composable
-fun ReadIntervalEdit(
- state: BleView.Accelerometer,
- onEvent: (AccelerometerContract.Event) -> Unit,
-){
-
- var value by remember(state.accelerometerState.readInterval) {
- mutableIntStateOf((state.accelerometerState.readInterval).toInt())
- }
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Интервал чтения",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- DurationPicker(
- modifier = Modifier.align(Alignment.CenterHorizontally),
- value = value
- ) {
- value = it
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(
- AccelerometerContract.Event.OnReadIntervalChanged(
- value.toLong()
- )
- )
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconContract.kt
index 0dacd82..51acdb6 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconContract.kt
@@ -4,19 +4,22 @@ 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.main.ThermometerContract.Effect.Navigation
import llc.arma.ble.domain.model.Ble
class BeaconContract {
sealed class Event : ViewEvent {
+ data object OnNavigateUp : Event()
+
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
object OnShowWriteBlePreview : Event()
- object OnPowerEdit : Event()
+ data object OnPowerEdit : Event()
data class OnBleChanged(
val ble: Ble.Beacon
@@ -26,7 +29,7 @@ class BeaconContract {
val tx: BleView.BleState.TX
) : Event()
- data class OnTxChanged(val tx: Int) : Event()
+ data class OnTxChanged(val tx: BleView.BleState.TX) : Event()
object OnNavigateUpClicked : Event()
@@ -36,7 +39,7 @@ class BeaconContract {
sealed class State : ViewState {
- object Loading : State()
+ data object Loading : State()
data class Display(
val origin: Ble.Beacon,
@@ -54,9 +57,9 @@ class BeaconContract {
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
- object Success : WriteState()
+ data object Success : WriteState()
- object Failure : WriteState()
+ data object Failure : WriteState()
}
@@ -66,19 +69,21 @@ class BeaconContract {
sealed class Effect : ViewSideEffect {
- object ShowPowerPicker : Effect()
+ data object HideWriteBlePreview : Effect()
- object HidePowerPicker : Effect()
-
- object HideWriteBlePreview : Effect()
-
- object ShowWriteBlePreview : Effect()
+ data object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() {
- object NavigateToChangePassword : Navigation()
+ data object Up : Navigation()
- object NavigateUp : Navigation()
+ data class PasswordForm(
+ val bleSerial: String
+ ) : Navigation()
+
+ data class TxSelector(
+ val tx: BleView.BleState.TX?
+ ) : Navigation()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconScreen.kt
index 4a312dc..48bf4f0 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconScreen.kt
@@ -3,7 +3,18 @@ package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -13,24 +24,35 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultRecipient
+import com.ramcosta.composedestinations.result.onResult
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.TxLevelSelector
import llc.arma.ble.app.ui.common.rememberBottomDialogState
+import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write
-import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
- WRITE, POWER_EDIT
+ WRITE
}
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BeaconScreen(
- ble: Ble.Beacon,
- onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit
+ bleSerial: String,
+ txSelectResult: ResultRecipient,
+ navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel()
@@ -42,10 +64,14 @@ fun BeaconScreen(
val bottomDialog = rememberBottomDialogState()
- LaunchedEffect("effect"){
+ txSelectResult.onResult {
+ viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
+ }
+
+ LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
- is BeaconContract.Effect.Navigation -> onNavigationEvent(it)
+
BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
@@ -54,22 +80,19 @@ fun BeaconScreen(
delay(100)
sheetPage = SheetPage.WRITE
}
- BeaconContract.Effect.HidePowerPicker -> launch {
- sheetPage = null
- }
- BeaconContract.Effect.ShowPowerPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.POWER_EDIT
- }
+
+ is BeaconContract.Effect.Navigation.PasswordForm ->
+ navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
+
+ is BeaconContract.Effect.Navigation.TxSelector ->
+ navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
+
+ BeaconContract.Effect.Navigation.Up ->
+ navigator.popBackStack()
}
}.launchIn(this)
}
- LaunchedEffect(ble){
- viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
- }
-
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
@@ -88,48 +111,67 @@ fun BeaconScreen(
}
}
- SheetPage.POWER_EDIT -> bottomDialog.show {
- val currentState = viewModel.viewState.value
-
- if(currentState is BeaconContract.State.Display) {
-
- TxLevelSelector(
- tx = currentState.beacon.state.tx
- ) {
- viewModel.setEvent(BeaconContract.Event.OnPowerChanged(it))
- }
-
- }
- }
else -> {
bottomDialog.hide()
}
}
}
- Column {
-
- when(state){
- is BeaconContract.State.Display -> DisplayState(
- onEvent = {
- viewModel.setEvent(it)
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
},
- ble = state.beacon,
- origin = state.origin
+ title = {
+ Text(text = BleInfo.Type.BEACON.localized)
+ }
)
- is BeaconContract.State.Loading -> LoadingState()
}
-
+ ) {
+
+ Box(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when(state){
+ is BeaconContract.State.Display -> DisplayState(
+ onEvent = {
+ viewModel.setEvent(it)
+ },
+ ble = state.beacon,
+ origin = state.origin
+ )
+ is BeaconContract.State.Loading -> LoadingState()
+ }
+
+ }
+
}
+
+
}
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
- Box(modifier = Modifier.fillMaxSize()){
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ ContainedLoadingIndicator()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconViewModel.kt
index 3a2b4f2..620a9f6 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/BeaconViewModel.kt
@@ -1,23 +1,74 @@
package llc.arma.ble.app.ui.screen.inspection.beacon
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
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.accelerometer.main.AccelerometerContract
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
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.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class BeaconViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel() {
+ init {
+
+ val params = BeaconScreenDestination.argsFrom(savedStateHandle)
+
+ viewModelScope.launch {
+
+ val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
+ onSuccess = { it },
+ onFailure = { null }
+ )
+
+ if(ble != null && ble is Ble.Beacon){
+ setState {
+
+ when(this){
+ is BeaconContract.State.Display -> {
+ copy(
+ origin = Ble.Beacon(
+ info = ble.info,
+ state = origin.state
+ )
+ )
+ }
+ BeaconContract.State.Loading -> {
+ BeaconContract.State.Display(
+ origin = ble,
+ beacon = bleMapper.map(ble) as BleView.Beacon,
+ writeState = null
+ )
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
override fun setInitialState() = BeaconContract.State.Loading
override fun handleEvents(event: BeaconContract.Event) {
@@ -31,6 +82,7 @@ class BeaconViewModel @Inject constructor(
is BeaconContract.Event.OnWriteBle -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event)
+ is BeaconContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
@@ -45,8 +97,15 @@ class BeaconViewModel @Inject constructor(
}
+ }
+
+ private fun reduce(
+ state: BeaconContract.State,
+ event: BeaconContract.Event.OnNavigateUp
+ ) {
+
setEffect {
- BeaconContract.Effect.HidePowerPicker
+ BeaconContract.Effect.Navigation.Up
}
}
@@ -56,7 +115,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnPowerEdit
) {
- setEffect { BeaconContract.Effect.ShowPowerPicker }
+
+ if(state is BeaconContract.State.Display) {
+
+ setEffect { BeaconContract.Effect.Navigation.TxSelector(state.beacon.state.tx) }
+
+ }
+
}
@@ -64,7 +129,7 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked
) {
- setEffect { BeaconContract.Effect.Navigation.NavigateUp }
+ setEffect { BeaconContract.Effect.Navigation.Up }
}
private fun reduce(
@@ -107,9 +172,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnChangePassword
) {
+
+ val params = BeaconScreenDestination.argsFrom(savedStateHandle)
+
setEffect {
- BeaconContract.Effect.Navigation.NavigateToChangePassword
+ BeaconContract.Effect.Navigation.PasswordForm(params.bleSerial)
}
+
}
private fun reduce(
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/DisplayState.kt
index 1d3ad36..58a8523 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/DisplayState.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/beacon/view/DisplayState.kt
@@ -1,8 +1,9 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.view
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -10,17 +11,20 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
+import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
-import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
+import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.domain.model.Ble
@Composable
@@ -30,64 +34,63 @@ fun DisplayState(
ble: BleView.Beacon
) {
- val scrollState = rememberScrollState()
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .verticalScroll(rememberScrollState())
+ ) {
- Column {
+ BleInfoView(
+ bleInfo = origin.info,
+ version = origin.state.version
+ )
Column(
- modifier = Modifier
- .verticalScroll(scrollState)
- .weight(1f)
+ verticalArrangement = Arrangement.spacedBy(2.dp)
) {
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
+ BleMenuItem(
+ shapeType = ShapeType.Start,
+ title = "Мощность",
+ subtitle = "${ble.state.tx.value} db",
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
) {
- BleInfoView(
- bleInfo = origin.info,
- version = origin.state.version
- )
+ onEvent(BeaconContract.Event.OnPowerEdit)
}
- Column(
- modifier = Modifier,
- content = {
-
- BleMenuItem(
- title = "Мощность",
- subtitle = "${ble.state.tx.value} db",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
- onEvent(BeaconContract.Event.OnPowerEdit)
- }
-
- BleMenuItem(
- title = "Изменить пароль",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
- onEvent(BeaconContract.Event.OnChangePassword)
- }
-
+ BleMenuItem(
+ shapeType = ShapeType.End,
+ title = "Изменить пароль",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
}
- )
+ ) {
+ onEvent(BeaconContract.Event.OnChangePassword)
+ }
}
- PrimaryButton(
- modifier = Modifier.shadow(
- if(scrollState.canScrollForward){
- 8.dp
- } else {
- 0.dp
- }
- ).background(MaterialTheme.colorScheme.background),
- label = "Сохранить"
+ Button (
+ onClick = {
+ onEvent(BeaconContract.Event.OnShowWriteBlePreview)
+ },
+ modifier = Modifier.fillMaxWidth()
) {
- onEvent(BeaconContract.Event.OnShowWriteBlePreview)
+
+ Text(
+ text = "Сохранить"
+ )
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryContract.kt
new file mode 100644
index 0000000..cb851e3
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryContract.kt
@@ -0,0 +1,48 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.history
+
+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.Ble
+import llc.arma.ble.domain.model.BleName
+
+class GateHistoryContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnNavigateUp : Event()
+
+ data object StopMeasure : Event()
+
+ data object OnRefreshHistory : Event()
+
+ data object OnExportHistory : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data class Loading(
+ val progress: Float?
+ ) : State()
+
+ data class Display(
+ val bleNames: List,
+ val loadingHistoryState: List
+ ) : State()
+
+ data object Exception : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data object Up : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryScreen.kt
new file mode 100644
index 0000000..2ce7e39
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryScreen.kt
@@ -0,0 +1,522 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.history
+
+import android.content.res.Configuration
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowColumn
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Refresh
+import androidx.compose.material.icons.rounded.Save
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.patrykandpatrick.vico.compose.axis.axisLineComponent
+import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
+import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
+import com.patrykandpatrick.vico.compose.chart.Chart
+import com.patrykandpatrick.vico.compose.chart.column.columnChart
+import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
+import com.patrykandpatrick.vico.compose.component.shapeComponent
+import com.patrykandpatrick.vico.compose.component.textComponent
+import com.patrykandpatrick.vico.core.axis.AxisPosition
+import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
+import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
+import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
+import com.patrykandpatrick.vico.core.component.shape.LineComponent
+import com.patrykandpatrick.vico.core.component.shape.Shapes.pillShape
+import com.patrykandpatrick.vico.core.dimensions.MutableDimensions
+import com.patrykandpatrick.vico.core.entry.ChartEntry
+import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
+import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer
+import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
+import com.patrykandpatrick.vico.core.scroll.InitialScroll
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class HostEntry(
+ val localDate: Long,
+ override val x: Float,
+ override val y: Float,
+) : ChartEntry {
+
+ override fun withY(y: Float) = HostEntry(localDate, x, y)
+
+}
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GateHistoryScreen(
+ bleSerial: String,
+ navigator: DestinationsNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect {
+ when(it){
+ GateHistoryContract.Effect.Navigation.Up ->
+ navigator.popBackStack()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(GateHistoryContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ val title = when(state){
+ is GateHistoryContract.State.Exception,
+ is GateHistoryContract.State.Loading -> "Таблица"
+ is GateHistoryContract.State.Display -> "Таблица (${state.loadingHistoryState.size})"
+ }
+
+ Text(
+ text = title,
+ )
+ },
+ actions = {
+
+ IconButton(
+ onClick = {
+ viewModel.setEvent(GateHistoryContract.Event.OnExportHistory)
+ },
+ enabled = when(state){
+ is GateHistoryContract.State.Display,
+ GateHistoryContract.State.Exception -> true
+ is GateHistoryContract.State.Loading -> false
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Save,
+ contentDescription = null
+ )
+ }
+
+ IconButton(
+ onClick = {
+ viewModel.setEvent(GateHistoryContract.Event.OnRefreshHistory)
+ },
+ enabled = when(state){
+ is GateHistoryContract.State.Display,
+ GateHistoryContract.State.Exception -> true
+ is GateHistoryContract.State.Loading -> false
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = null
+ )
+ }
+ }
+ )
+ }
+ ) {
+ Box(modifier = Modifier.padding(it)) {
+
+ when (state) {
+ is GateHistoryContract.State.Display -> DisplayState(state = state)
+ is GateHistoryContract.State.Exception -> ErrorState()
+ is GateHistoryContract.State.Loading -> LoadingState(state = state)
+ }
+
+ }
+ }
+
+}
+
+val dayFormatter = SimpleDateFormat("dd", Locale.getDefault())
+val dateFormatter = SimpleDateFormat("dd.MM", Locale.getDefault())
+val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
+
+val colorsStack = listOf(
+ Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
+ Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
+ Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
+ Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
+ Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
+ Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
+ Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
+ Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
+ Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
+ Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
+ Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
+ Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
+ Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
+ Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
+ Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
+ Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
+)
+
+val startAxisValueFormatter =
+ AxisValueFormatter { value, chartValues ->
+ " "
+ }
+
+val axisValueFormatter =
+ AxisValueFormatter { value, chartValues ->
+ val first = (chartValues.chartEntryModel.entries.firstOrNull()?.firstOrNull() as? HostEntry)
+ val last = (chartValues.chartEntryModel.entries.firstOrNull()?.lastOrNull() as? HostEntry)
+ val previous = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt() - 1) as? HostEntry)
+ val current = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt()) as? HostEntry)
+
+ if(current != null) {
+ if (first == current || last == current) {
+ dateFormatter.format(Date(current.localDate))
+ } else {
+ if(previous != null && dayFormatter.format(previous.localDate) != dayFormatter.format(current.localDate)){
+ dateFormatter.format(Date(current.localDate))
+ }else{
+ timeFormatter.format(Date(current.localDate))
+ }
+ }.orEmpty()
+ } else {
+ " "
+ }
+ }
+
+@Composable
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+private fun LoadingState(
+ state: GateHistoryContract.State.Loading
+){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
+
+ if(state.progress == null){
+ ContainedLoadingIndicator()
+ }else{
+ ContainedLoadingIndicator(
+ progress = { state.progress }
+ )
+ }
+
+ }
+
+}
+
+@Composable
+private fun DisplayState(
+ state: GateHistoryContract.State.Display
+) {
+
+ if (state.loadingHistoryState.isEmpty()) {
+
+ EmptyState()
+
+ } else {
+
+ DataState(state)
+
+ }
+
+}
+
+@Composable
+private fun EmptyState(){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "Нет данных"
+ )
+
+ }
+
+}
+
+@Composable
+private fun DataState(
+ state: GateHistoryContract.State.Display
+){
+
+ val configuration = LocalConfiguration.current
+
+ val allSerials =
+ remember(state) { state.loadingHistoryState.flatMap { it.value }.distinct() }
+
+ val colors = remember(allSerials) {
+ allSerials.mapIndexed { index, s ->
+ Pair(s, colorsStack[index])
+ }.toMap()
+ }
+
+ var selectedSerials by rememberSaveable {
+ mutableStateOf(allSerials.take(1))
+ }
+
+ val serials =
+ remember(selectedSerials) { allSerials.filter { selectedSerials.contains(it) } }
+
+ val entries = remember(serials, state) {
+
+ serials.map { serial ->
+
+ ChartEntryModelProducer(
+ state.loadingHistoryState.mapIndexed { index, historyPoint ->
+ if (historyPoint.value.contains(serial)) {
+ HostEntry(historyPoint.date, index.toFloat(), 1f)
+ } else {
+ HostEntry(historyPoint.date, index.toFloat(), 0.0001f)
+ }
+ }
+ )
+
+ }
+
+ }
+
+ val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
+
+ val chart = columnChart(
+ persistentMarkers = state.loadingHistoryState.mapIndexedNotNull { index, historyPoint ->
+ if (historyPoint.hit) {
+ Pair(
+ index.toFloat(),
+ MarkerComponent(
+ label = textComponent(
+ textSize = 0.sp,
+ padding = MutableDimensions(10f, 10f, 10f, 10f),
+ margins = MutableDimensions(10f, 10f, 10f, 10f),
+ color = Color.Red,
+ background = shapeComponent(
+ color = Color.Red,
+ shape = pillShape
+ )
+ ),
+ indicator = null,
+ guideline = axisLineComponent(
+ thickness = 0.dp
+ )
+ )
+ )
+ } else {
+ null
+ }
+ }.toMap(),
+ innerSpacing = 2.dp,
+ columns = serials.map {
+ LineComponent(
+ color = colors[it]!!.toArgb(),
+ thicknessDp = 7f,
+ shape = pillShape
+ )
+ },
+ spacing = 8.dp,
+ )
+
+ @Composable
+ fun LegendItem(s: String) {
+
+ FilterChip(
+ selected = selectedSerials.contains(s),
+ onClick = {
+ selectedSerials = if (selectedSerials.contains(s)) {
+ selectedSerials.toMutableList().apply {
+ remove(s)
+ }
+ } else {
+ selectedSerials.toMutableList().apply {
+ add(s)
+ }
+ }
+ },
+ leadingIcon = {
+ Surface(
+ shape = CircleShape,
+ color = colors[s]!!,
+ modifier = Modifier.size(28.dp)
+ ) {}
+ },
+ label = {
+ Column {
+ Text(text = state.bleNames.firstOrNull { it.serial == s }?.name ?: s)
+ Text(
+ style = MaterialTheme.typography.bodySmall.copy(
+ color = LocalTextStyle.current.color.copy(
+ alpha = ContentAlpha.medium
+ )
+ ),
+ text = s
+ )
+ }
+ }
+ )
+
+ }
+
+ when (configuration.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE -> {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+
+ Chart(
+ chart = chart,
+ chartModelProducer = producer,
+ bottomAxis = bottomAxis(
+ labelRotationDegrees = -90f,
+ valueFormatter = axisValueFormatter,
+ tickLength = 0.dp,
+ ),
+ startAxis = startAxis(
+ valueFormatter = startAxisValueFormatter
+ ),
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(1f),
+ autoScaleUp = AutoScaleUp.None,
+ diffAnimationSpec = tween(0),
+ chartScrollSpec = rememberChartScrollSpec(
+ initialScroll = InitialScroll.End,
+ autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
+ autoScrollAnimationSpec = tween(0)
+ )
+ )
+
+ VerticalDivider()
+
+ FlowRow(
+ maxItemsInEachRow = 1,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
+
+ allSerials.mapIndexed { _, s ->
+ LegendItem(s = s)
+ }
+
+ }
+
+ }
+ }
+
+ else -> {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+
+ FlowColumn(
+ maxItemsInEachColumn = 2,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.horizontalScroll(rememberScrollState())
+ ) {
+
+ allSerials.mapIndexed { _, s ->
+ LegendItem(s = s)
+ }
+
+ }
+
+ HorizontalDivider()
+
+ Chart(
+
+ chart = chart,
+ chartModelProducer = producer,
+ bottomAxis = bottomAxis(
+ labelRotationDegrees = -90f,
+ valueFormatter = axisValueFormatter,
+ tickLength = 0.dp,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ autoScaleUp = AutoScaleUp.None,
+ diffAnimationSpec = tween(0),
+ chartScrollSpec = rememberChartScrollSpec(
+ initialScroll = InitialScroll.End,
+ autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
+ autoScrollAnimationSpec = tween(0)
+ )
+ )
+
+ }
+ }
+ }
+
+}
+
+@Composable
+private fun ErrorState() {
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ .aspectRatio(2f),
+ ){
+
+ Text(
+ textAlign = TextAlign.Center,
+ text = "Во время загрузки произошла ошибка",
+ modifier = Modifier.align(Alignment.Center)
+ )
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryViewModel.kt
new file mode 100644
index 0000000..feb7264
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/history/GateHistoryViewModel.kt
@@ -0,0 +1,152 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.history
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
+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.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.usecase.ExportToXlsx
+import llc.arma.ble.domain.usecase.GetBleNamesFlow
+import llc.arma.ble.domain.usecase.GetHostHistoryBySerial
+import javax.inject.Inject
+
+@HiltViewModel
+class GateHistoryViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val getHostHistoryBySerial: GetHostHistoryBySerial,
+ private val getBleNamesFlow: GetBleNamesFlow,
+ private val exportToXlsx: ExportToXlsx
+) : BaseViewModel() {
+
+ private var measureJob: Job? = null
+
+ init {
+
+ loadData()
+
+ }
+
+ override fun setInitialState() = GateHistoryContract.State.Loading(null)
+
+ override fun handleEvents(event: GateHistoryContract.Event) {
+ when(event){
+ is GateHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
+ is GateHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
+ is GateHistoryContract.Event.OnExportHistory -> reduce(viewState.value, event)
+ is GateHistoryContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: GateHistoryContract.State,
+ event: GateHistoryContract.Event.OnNavigateUp
+ ) {
+
+ setEffect {
+ GateHistoryContract.Effect.Navigation.Up
+ }
+
+ }
+
+ private fun reduce(
+ state: GateHistoryContract.State,
+ event: GateHistoryContract.Event.OnExportHistory
+ ) {
+
+ val params = GateHistoryScreenDestination.argsFrom(savedStateHandle)
+
+ if(state is GateHistoryContract.State.Display) {
+
+ exportToXlsx.invoke(params.bleSerial, state.loadingHistoryState)
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateHistoryContract.State,
+ event: GateHistoryContract.Event.StopMeasure
+ ) {
+
+ measureJob?.cancel()
+ measureJob = null
+
+ setState {
+ GateHistoryContract.State.Exception
+ }
+
+ }
+
+ private fun reduce(
+ state: GateHistoryContract.State,
+ event: GateHistoryContract.Event.OnRefreshHistory
+ ) {
+
+ viewModelScope.launch { loadData() }
+
+ }
+
+ private fun loadData(){
+
+ val state = viewState.value
+ val params = GateHistoryScreenDestination.argsFrom(savedStateHandle)
+
+ viewModelScope.launch {
+
+ setState { GateHistoryContract.State.Loading(null) }
+
+ measureJob?.cancel()
+ measureJob = null
+
+ val names = getBleNamesFlow().first()
+
+ measureJob = getHostHistoryBySerial(params.bleSerial).onEach {
+ it.fold(
+ onSuccess = {
+
+ when (it) {
+ is ProgressState.Finished> -> {
+ setState {
+ GateHistoryContract.State.Display(
+ names,
+ it.data
+ )
+ }
+ }
+
+ is ProgressState.Indeterminate -> {
+ setState {
+ GateHistoryContract.State.Loading(null)
+ }
+ }
+
+ is ProgressState.Progress -> {
+ setState {
+ GateHistoryContract.State.Loading(it.value)
+ }
+ }
+ }
+
+
+ },
+ onFailure = {
+ setState {
+ GateHistoryContract.State.Exception
+ }
+ }
+ )
+ }.launchIn(this)
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateContract.kt
similarity index 59%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateContract.kt
index dc99813..a4d24d9 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateContract.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host
+package llc.arma.ble.app.ui.screen.inspection.gate.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@@ -7,7 +7,7 @@ import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
-class HostContract {
+class GateContract {
sealed class Event : ViewEvent {
@@ -17,19 +17,13 @@ class HostContract {
data object OnShowWriteBlePreview : Event()
- data object OnPowerEdit : Event()
-
- data class OnBleChanged(
- val ble: Ble.Host
- ) : Event()
+ data object OnTxSelect : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
- data class OnTxChanged(val tx: Int) : Event()
-
- data object OnShowIntervalEdit : Event()
+ data object OnHistoryIntervalSelect : Event()
data class OnSaveIntervalChanged(
val interval: Long
@@ -41,7 +35,7 @@ class HostContract {
val interval: Long
) : Event()
- data object OnNavigateUpClicked : Event()
+ data object OnNavigateUp : Event()
data object OnChangePassword : Event()
@@ -53,22 +47,24 @@ class HostContract {
sealed class State : ViewState {
- data object Loading : State()
+ data class Loading(
+ val attempt: Int?
+ ) : State()
data class Display(
- val origin: Ble.Host,
- val host: BleView.Host,
+ val origin: Ble.Gate,
+ val gate: BleView.Gate,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
- val writeRequest: Ble.Host.WriteRequest
+ val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data class Writing(
- val writeRequest: Ble.Host.WriteRequest
+ val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data object Success : WriteState()
@@ -83,36 +79,36 @@ class HostContract {
sealed class Effect : ViewSideEffect {
- data object ShowPowerPicker : Effect()
-
- data object HidePowerPicker : Effect()
-
- data object HideWriteBlePreview : Effect()
-
data object ShowWriteBlePreview : Effect()
- data object HideIntervalPicker : Effect()
-
- data object ShowIntervalPicker : Effect()
-
- data object HideReadIntervalPicker : Effect()
-
- data object ShowReadIntervalPicker : Effect()
-
sealed class Navigation : Effect() {
- data object NavigateToChangePassword : Navigation()
+ data class ChangePassword(
+ val serial: String,
+ ) : Navigation()
- data object NavigateUp : Navigation()
+ data object Up : Navigation()
- data class NavigateToHostHistory(
+ data class GateHistory(
val ble: BleInfo,
) : Navigation()
- data class NavigateToBleTable(
+ data class BleTable(
val serial: String,
) : Navigation()
+ data class TxSelector(
+ val tx: BleView.BleState.TX?
+ ) : Navigation()
+
+ data class ReadIntervalSelector(
+ val interval: Int
+ ) : Navigation()
+
+ data class HistoryIntervalSelector(
+ val interval: Int
+ ) : Navigation()
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateScreen.kt
new file mode 100644
index 0000000..7c8bd43
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateScreen.kt
@@ -0,0 +1,255 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.main
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultRecipient
+import com.ramcosta.composedestinations.result.onResult
+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.rememberBottomDialogState
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState
+import llc.arma.ble.app.ui.screen.inspection.gate.main.view.Write
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleInfo
+
+enum class SheetPage {
+ WRITE
+}
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun GateScreen(
+ bleSerial: String,
+ readDurationSelectResult: ResultRecipient,
+ txSelectResult: ResultRecipient,
+ navigator: DestinationsNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ var sheetPage by rememberSaveable {
+ mutableStateOf(null)
+ }
+
+ val bottomDialog = rememberBottomDialogState()
+
+ txSelectResult.onResult {
+ viewModel.setEvent(GateContract.Event.OnPowerChanged(it))
+ }
+
+ readDurationSelectResult.onResult {
+
+ if(it.qualifier == "ReadIntervalSelector"){
+ viewModel.setEvent(GateContract.Event.OnSaveReadIntervalChanged(it.duration.toLong()))
+ }
+
+ if(it.qualifier == "HistoryIntervalSelector"){
+ viewModel.setEvent(GateContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
+ }
+
+ }
+
+ LaunchedEffect(Unit){
+ viewModel.effect.onEach {
+ when(it){
+ GateContract.Effect.ShowWriteBlePreview -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
+
+ is GateContract.Effect.Navigation.BleTable ->
+ navigator.navigate(GateBleTableScreenDestination(it.serial))
+
+ is GateContract.Effect.Navigation.ChangePassword ->
+ navigator.navigate(ChangePasswordScreenDestination(it.serial))
+
+ is GateContract.Effect.Navigation.GateHistory ->
+ navigator.navigate(GateHistoryScreenDestination(it.ble.serial))
+
+ is GateContract.Effect.Navigation.ReadIntervalSelector ->
+ navigator.navigate(DurationSelectorScreenDestination(
+ qualifier = "ReadIntervalSelector",
+ duration = it.interval,
+ maximum = 10 * 24 * 60 * 60 * 1000
+ ))
+
+ is GateContract.Effect.Navigation.TxSelector ->
+ navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
+
+ GateContract.Effect.Navigation.Up ->
+ navigator.navigateUp()
+
+ is GateContract.Effect.Navigation.HistoryIntervalSelector ->
+ navigator.navigate(DurationSelectorScreenDestination(
+ qualifier = "HistoryIntervalSelector",
+ duration = it.interval,
+ maximum = 10 * 24 * 60 * 60 * 1000
+ ))
+
+ }
+ }.launchIn(this)
+ }
+
+ LaunchedEffect(sheetPage){
+ when(sheetPage){
+ SheetPage.WRITE -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if(currentState is GateContract.State.Display && currentState.writeState != null) {
+
+ Write(
+ state = currentState.writeState,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+
+ }
+
+ }
+
+ else -> {
+ bottomDialog.hide()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(GateContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text(
+ text = BleInfo.Type.HOST.localized
+ )
+ }
+ )
+ }
+ ) {
+
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when (state) {
+ is GateContract.State.Display -> DisplayState(viewModel, state)
+ is GateContract.State.Loading -> LoadingState(viewModel, state)
+ }
+
+ }
+
+ }
+
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun LoadingState(
+ viewModel: GateViewModel,
+ state: GateContract.State.Loading
+){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.widthIn(max = 230.dp)
+ ) {
+
+ ContainedLoadingIndicator()
+
+ state.attempt?.let {
+
+ Spacer(Modifier.height(16.dp))
+
+ Text(
+ text = "Повторная попытка ${it}"
+ )
+
+ Text(
+ text = "Во время загрузки произошла ошибка",
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodySmall
+ )
+
+ Spacer(Modifier.height(8.dp))
+
+ TextButton(
+ onClick = {
+ viewModel.setEvent(GateContract.Event.OnNavigateUp)
+ }
+ ) {
+
+ Text(
+ text = "Отмена"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateViewModel.kt
new file mode 100644
index 0000000..fcdfcb4
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/GateViewModel.kt
@@ -0,0 +1,349 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.main
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+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.domain.model.Ble
+import llc.arma.ble.domain.usecase.GetBleBySerial
+import llc.arma.ble.domain.usecase.WriteBle
+import javax.inject.Inject
+
+@HiltViewModel
+class GateViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val getBleBySerial: GetBleBySerial,
+ private val bleMapper: BleMapper,
+ private val writeBle: WriteBle,
+ private val bleViewMapper: BleViewMapper
+) : BaseViewModel() {
+
+ init {
+
+ val params = GateScreenDestination.argsFrom(savedStateHandle)
+
+ viewModelScope.launch {
+
+ var attempt = 0
+
+ var ble: Ble? = null
+
+ while (ble == null) {
+
+ ble = getBleBySerial.invoke(params.bleSerial, this).fold(
+ onSuccess = { return@fold it },
+ onFailure = { return@fold null }
+ )
+
+ if(ble == null) {
+
+ setState {
+ attempt++
+ GateContract.State.Loading(attempt)
+ }
+
+ }
+
+ }
+
+ if (ble is Ble.Gate) {
+
+ setState {
+
+ when (this) {
+ is GateContract.State.Display -> {
+ copy(
+ origin = Ble.Gate(
+ info = ble.info,
+ state = origin.state,
+ gateState = origin.gateState
+ )
+ )
+ }
+
+ is GateContract.State.Loading -> {
+ GateContract.State.Display(
+ origin = ble,
+ gate = bleMapper.map(ble) as BleView.Gate,
+ writeState = null
+ )
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+ }
+
+ override fun setInitialState() = GateContract.State.Loading(null)
+
+ override fun handleEvents(event: GateContract.Event) {
+ when(event){
+ is GateContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ is GateContract.Event.OnChangePassword -> reduce(viewState.value, event)
+ is GateContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
+ is GateContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
+ is GateContract.Event.OnWriteBle -> reduce(viewState.value, event)
+ is GateContract.Event.OnPowerChanged -> reduce(viewState.value, event)
+ is GateContract.Event.OnTxSelect -> reduce(viewState.value, event)
+ is GateContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
+ is GateContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
+ is GateContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
+ is GateContract.Event.OnHistoryIntervalSelect -> reduce(viewState.value, event)
+ is GateContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event)
+ is GateContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnSaveReadIntervalChanged
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ state.gate.hostState.readInterval = event.interval
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnShowReadIntervalEdit
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect {
+ GateContract.Effect.Navigation.ReadIntervalSelector(state.gate.hostState.readInterval.toInt())
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnSaveIntervalChanged
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ state.gate.hostState.historyInterval = event.interval
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnHistoryIntervalSelect
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect {
+ GateContract.Effect.Navigation.HistoryIntervalSelector(state.gate.hostState.historyInterval.toInt())
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnShowHostBleTable
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect {
+ GateContract.Effect.Navigation.BleTable(state.gate.info.serial)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnShowHostHistory
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect {
+ GateContract.Effect.Navigation.GateHistory(state.gate.info)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnPowerChanged
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ state.gate.state.tx = event.tx
+
+ }
+
+ }
+
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnTxSelect
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect { GateContract.Effect.Navigation.TxSelector(state.gate.state.tx) }
+
+ }
+
+ }
+
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnNavigateUp
+ ) {
+ setEffect { GateContract.Effect.Navigation.Up }
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnChangePassword
+ ) {
+
+ if(state is GateContract.State.Display) {
+
+ setEffect {
+ GateContract.Effect.Navigation.ChangePassword(state.gate.info.serial)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnHideWriteBlePreview
+ ) {
+
+
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnShowWriteBlePreview
+ ) {
+
+ if(state is GateContract.State.Display){
+
+ val newBle = bleViewMapper.map(state.gate) as Ble.Gate
+
+ val writeRequest = Ble.Gate.WriteRequest(
+ tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
+ interval = if(newBle.gateState.historyInterval == state.origin.gateState.historyInterval) null else newBle.gateState.historyInterval,
+ readInterval = if(newBle.gateState.readInterval == state.origin.gateState.readInterval) null else newBle.gateState.readInterval,
+ )
+
+ setState {
+ state.copy(
+ writeState = GateContract.State.Display.WriteState.DisplayPreview(
+ writeRequest
+ )
+ )
+ }
+
+ setEffect {
+ GateContract.Effect.ShowWriteBlePreview
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: GateContract.State,
+ event: GateContract.Event.OnWriteBle
+ ) {
+
+ if(state is GateContract.State.Display){
+
+ state.writeState?.let { request ->
+
+ if(request is GateContract.State.Display.WriteState.DisplayPreview) {
+
+ viewModelScope.launch {
+
+ setState {
+ state.copy(
+ writeState = GateContract.State.Display.WriteState.Writing(request.writeRequest)
+ )
+ }
+
+ val currentState = viewState.value
+
+ if(currentState is GateContract.State.Display) {
+
+ val newBleObject = Ble.Gate(
+ info = currentState.origin.info,
+ state = currentState.origin.state.copy(
+ tx = request.writeRequest.tx ?: state.origin.state.tx
+ ),
+ gateState = currentState.origin.gateState.copy(
+ historyInterval = request.writeRequest.interval
+ ?: currentState.origin.gateState.historyInterval
+ )
+ )
+
+ writeBle(state.gate.info.serial, request.writeRequest).fold(
+ onSuccess = {
+ setState {
+ currentState.copy(
+ origin = newBleObject,
+ gate = bleMapper.map(newBleObject) as BleView.Gate,
+ writeState = GateContract.State.Display.WriteState.Success
+ )
+ }
+ },
+ onFailure = {
+ setState {
+ state.copy(
+ writeState = GateContract.State.Display.WriteState.Failure
+ )
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/DisplayState.kt
new file mode 100644
index 0000000..27ba487
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/DisplayState.kt
@@ -0,0 +1,158 @@
+package llc.arma.ble.app.ui.screen.inspection.gate.main.view
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.BleInfoView
+import llc.arma.ble.app.ui.screen.ShapeType
+import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
+import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
+import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
+import llc.arma.ble.domain.model.Ble
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@Composable
+fun DisplayState(
+ viewModel: GateViewModel,
+ state: GateContract.State.Display
+) {
+
+ val scrollState = rememberScrollState()
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ .padding(horizontal = 16.dp)
+ ) {
+
+ BleInfoView(
+ bleInfo = state.origin.info,
+ version = state.origin.state.version
+ )
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+
+ BleMenuItem(
+ shapeType = ShapeType.Start,
+ title = "Мощность",
+ subtitle = "${state.gate.state.tx.value} db",
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnTxSelect)
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Интервал измерений",
+ subtitle = state.gate.hostState.historyInterval
+ .toDuration(DurationUnit.MILLISECONDS)
+ .toComponents { hours, minutes, seconds, _ ->
+ "$hours ч. $minutes мин. $seconds сек." },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnHistoryIntervalSelect)
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Интервал чтения",
+ subtitle = state.gate.hostState.readInterval
+ .toDuration(DurationUnit.MILLISECONDS)
+ .toComponents { hours, minutes, seconds, _ ->
+ "$hours ч. $minutes мин. $seconds сек." },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnShowReadIntervalEdit)
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "График измерений",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnShowHostHistory)
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Таблица BLE ID",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnShowHostBleTable)
+ }
+
+ BleMenuItem(
+ shapeType = ShapeType.End,
+ title = "Изменить пароль",
+ icon = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = null
+ )
+ }
+ ) {
+ viewModel.setEvent(GateContract.Event.OnChangePassword)
+ }
+
+ }
+
+ Button(
+ onClick = {
+ viewModel.setEvent(GateContract.Event.OnShowWriteBlePreview)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ ) {
+ Text(text = "Сохранить")
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/Write.kt
similarity index 90%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/Write.kt
index a86501a..3d41111 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/main/view/Write.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view
+package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@@ -24,13 +24,16 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
+import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
fun Write(
- state: HostContract.State.Display.WriteState,
- onEvent: (HostContract.Event) -> Unit
+ state: GateContract.State.Display.WriteState,
+ onEvent: (GateContract.Event) -> Unit
) {
Column(
@@ -46,7 +49,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp))
when (state) {
- is HostContract.State.Display.WriteState.DisplayPreview -> {
+ is GateContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.interval != null || state.writeRequest.readInterval !== null) {
@@ -174,13 +177,13 @@ fun Write(
PrimaryButton(
label = "Записать"
) {
- onEvent(HostContract.Event.OnWriteBle)
+ onEvent(GateContract.Event.OnWriteBle)
}
SecondaryButton(
label = "Отменить"
) {
- onEvent(HostContract.Event.OnHideWriteBlePreview)
+ onEvent(GateContract.Event.OnHideWriteBlePreview)
}
} else {
@@ -198,14 +201,14 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(HostContract.Event.OnHideWriteBlePreview)
+ onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
- is HostContract.State.Display.WriteState.Writing -> {
+ is GateContract.State.Display.WriteState.Writing -> {
Box {
@@ -224,7 +227,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
- onEvent(HostContract.Event.OnHideWriteBlePreview)
+ onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
@@ -232,7 +235,7 @@ fun Write(
}
}
- HostContract.State.Display.WriteState.Success -> {
+ GateContract.State.Display.WriteState.Success -> {
Box {
@@ -266,7 +269,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(HostContract.Event.OnHideWriteBlePreview)
+ onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
@@ -274,7 +277,7 @@ fun Write(
}
}
- HostContract.State.Display.WriteState.Failure -> {
+ GateContract.State.Display.WriteState.Failure -> {
Box {
@@ -308,7 +311,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
- onEvent(HostContract.Event.OnHideWriteBlePreview)
+ onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableContract.kt
similarity index 85%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableContract.kt
index 0e45adb..7d5b144 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableContract.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view.table
+package llc.arma.ble.app.ui.screen.inspection.gate.table
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@@ -6,7 +6,7 @@ import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
-class BleTableEditContract {
+class GateBleTableContract {
sealed class Event : ViewEvent {
@@ -16,9 +16,7 @@ class BleTableEditContract {
data object OnWrite: Event()
- data class OnStart(
- val serial: String
- ) : Event()
+ data object OnRestart : Event()
data class OnAddBle(
val ble: BleName
@@ -63,7 +61,7 @@ class BleTableEditContract {
sealed class Navigation : Effect() {
- data object NavigateUp : Navigation()
+ data object Up : Navigation()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableScreen.kt
similarity index 64%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableScreen.kt
index 78a204b..c167826 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableScreen.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view.table
+package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
@@ -13,15 +13,14 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Add
-import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material3.Checkbox
-import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -38,40 +37,42 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.rememberBottomDialogState
+import llc.arma.ble.app.ui.screen.ShapeType
+import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.ble.BleItem
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
+@Destination
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
-fun BleTableEditScreen(
- serial: String,
- onEvent: (event: BleTableEditContract.Effect.Navigation) -> Unit
+fun GateBleTableScreen(
+ bleSerial: String,
+ navigator: DestinationsNavigator
) {
- val viewModel = hiltViewModel()
+ val viewModel = hiltViewModel()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
- viewModel.effect.onEach {
+ viewModel.effect.collect {
when(it){
- is BleTableEditContract.Effect.Navigation -> onEvent(it)
+ GateBleTableContract.Effect.Navigation.Up ->
+ navigator.navigateUp()
}
- }.launchIn(this)
- }
-
- LaunchedEffect(key1 = serial) {
- viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
+ }
}
var showSelector by remember {
@@ -102,7 +103,7 @@ fun BleTableEditScreen(
if(showSelector){
showSelector = false
} else {
- onEvent(BleTableEditContract.Effect.Navigation.NavigateUp)
+ navigator.popBackStack()
}
}) {
Icon(
@@ -125,7 +126,7 @@ fun BleTableEditScreen(
actions = {
if(showSelector.not()){
IconButton(
- enabled = state is BleTableEditContract.State.Display,
+ enabled = state is GateBleTableContract.State.Display,
onClick = { showSelector=true }
) {
Icon(
@@ -138,20 +139,20 @@ fun BleTableEditScreen(
)
- if(state is BleTableEditContract.State.Loading){
+ if(state is GateBleTableContract.State.Loading){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
- CircularProgressIndicator()
+ ContainedLoadingIndicator()
}
}
- if(state is BleTableEditContract.State.Error){
+ if(state is GateBleTableContract.State.Error){
Box(
contentAlignment = Alignment.Center,
@@ -172,7 +173,7 @@ fun BleTableEditScreen(
PrimaryButton(
label = "Повторить"
) {
- viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
+ viewModel.setEvent(GateBleTableContract.Event.OnRestart)
}
}
@@ -181,7 +182,7 @@ fun BleTableEditScreen(
}
- if (state is BleTableEditContract.State.Display) {
+ if (state is GateBleTableContract.State.Display) {
if(showSelector) {
@@ -194,7 +195,7 @@ fun BleTableEditScreen(
}
) {
viewModel.setEvent(
- BleTableEditContract.Event.OnAddBle(
+ GateBleTableContract.Event.OnAddBle(
BleName(
serial = it.serial,
name = it.name
@@ -210,9 +211,10 @@ fun BleTableEditScreen(
}
LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.weight(1f)
- .padding(horizontal = 12.dp)
+ .padding(horizontal = 16.dp)
) {
val savedBleSerials = state.savedBleTable.map { it.serial }
@@ -222,45 +224,76 @@ fun BleTableEditScreen(
item {
Text(
- style = MaterialTheme.typography.titleLarge,
- textAlign = TextAlign.Center,
text = "Новые BLE",
+ modifier = Modifier
+ .padding(
+ horizontal = 12.dp,
+ vertical = 8.dp
+ )
)
}
items(items = newBle) {
+
SelectBleItem(
ble = it,
onClick = {
editBle = it
- viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
+ viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
+ },
+ shapeType = if(newBle.size == 1){
+ ShapeType.Singleton
+ } else {
+ if(newBle.indexOf(it) == 0){
+ ShapeType.Start
+ } else {
+ if(newBle.indexOf(it) == newBle.size - 1){
+ ShapeType.End
+ } else {
+ ShapeType.Middle
+ }
+ }
}
) {
- viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
+ viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
}
}
}
- if (state.savedBleTable.isNotEmpty()) {
+ item {
- item {
- Text(
- style = MaterialTheme.typography.titleLarge,
- textAlign = TextAlign.Center,
- text = "Сохраненные BLE",
- )
- }
+ Text(
+ text = "Сохраненные BLE",
+ modifier = Modifier
+ .padding(
+ horizontal = 12.dp,
+ vertical = 8.dp
+ )
+ )
- items(items = state.savedBleTable) { ble ->
- SavedBleItem(
- checked = state.newTable.any { it.serial == ble.serial},
- ble = ble
- ){
- viewModel.setEvent(BleTableEditContract.Event.OnAddBle(ble))
+ }
+
+ items(items = state.savedBleTable) { ble ->
+ SavedBleItem(
+ checked = state.newTable.any { it.serial == ble.serial},
+ ble = ble,
+ shapeType = if(state.savedBleTable.size == 1){
+ ShapeType.Singleton
+ } else {
+ if(state.savedBleTable.indexOf(ble) == 0){
+ ShapeType.Start
+ } else {
+ if(state.savedBleTable.indexOf(ble) == state.savedBleTable.size - 1){
+ ShapeType.End
+ } else {
+ ShapeType.Middle
+ }
+ }
}
+ ){
+ viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble))
}
-
}
}
@@ -268,14 +301,14 @@ fun BleTableEditScreen(
PrimaryButton(
label = "Записать"
) {
- viewModel.setEvent(BleTableEditContract.Event.OnWritePreview)
+ viewModel.setEvent(GateBleTableContract.Event.OnWritePreview)
}
if(editBle != null){
Dialog(
onDismissRequest = {
- viewModel.setEvent(BleTableEditContract.Event.OnAddBle(ble = editBle!!.copy()))
+ viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble = editBle!!.copy()))
editBle = null
}
) {
@@ -310,7 +343,7 @@ fun BleTableEditScreen(
label = "Сохранить"
) {
viewModel.setEvent(
- BleTableEditContract.Event.OnAddBle(
+ GateBleTableContract.Event.OnAddBle(
ble = editBle!!.copy(name = name)
)
)
@@ -327,7 +360,7 @@ fun BleTableEditScreen(
LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) {
if (bottomDialog.sheetState?.isVisible?.not() == true) {
- viewModel.setEvent(BleTableEditContract.Event.OnHideWritePreview)
+ viewModel.setEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
@@ -356,6 +389,11 @@ fun BleTableEditScreen(
}
+@Composable
+private fun DisplayState(){
+
+}
+
@Composable
fun BleSelectorScreen(
saved: List,
@@ -370,6 +408,7 @@ fun BleSelectorScreen(
) {
LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f)
) {
@@ -390,7 +429,10 @@ fun BleSelectorScreen(
onCheckedChange = null
)
- BleItem(ble = ble) {
+ BleItem(
+ shapeType = bleList.filterNot { saved.contains(it.serial) }.takeShapeType(ble),
+ ble = ble
+ ) {
onAddBle(ble)
}
@@ -412,46 +454,56 @@ fun BleSelectorScreen(
@Composable
fun SelectBleItem(
+ shapeType: ShapeType,
ble: BleName,
onClick: (() -> Unit)? = null,
onRemove: (() -> Unit)? = null,
){
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp),
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(16.dp))
- .clickable { onClick?.invoke() }
- .padding(vertical = 8.dp, horizontal = 16.dp)
-
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = shapeType.shape,
+ onClick = onClick ?: {}
) {
- Column(
- modifier = Modifier.weight(1f)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ vertical = 8.dp,
+ horizontal = 16.dp
+ )
+
) {
- Text(text = ble.name)
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
- Text(
- style = MaterialTheme.typography.bodyMedium,
- text = ble.serial
- )
+ Text(text = ble.name)
- }
-
- onRemove?.let {
-
- IconButton(onClick = onRemove) {
-
- Icon(
- imageVector = Icons.Rounded.RemoveCircleOutline,
- contentDescription = null
+ Text(
+ style = MaterialTheme.typography.bodyMedium,
+ text = ble.serial
)
}
+ onRemove?.let {
+
+ IconButton(onClick = onRemove) {
+
+ Icon(
+ imageVector = Icons.Rounded.RemoveCircleOutline,
+ contentDescription = null
+ )
+
+ }
+
+ }
+
}
}
@@ -460,31 +512,38 @@ fun SelectBleItem(
@Composable
fun SavedBleItem(
+ shapeType: ShapeType,
checked: Boolean,
ble: BleName,
onClick: () -> Unit
){
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(16.dp))
- .clickable { onClick() }
- .padding(vertical = 8.dp, horizontal = 16.dp)
- .padding(end = 12.dp)
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = shapeType.shape,
+ onClick = onClick
) {
- Column {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp, horizontal = 16.dp)
+ .padding(end = 12.dp)
+ ) {
- Text(text = ble.name)
- Text(text = ble.serial)
+ Column {
+
+ Text(text = ble.name)
+ Text(text = ble.serial)
+
+ }
+
+ Checkbox(checked = checked, onCheckedChange = null)
}
- Checkbox(checked = checked, onCheckedChange = null)
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableViewModel.kt
similarity index 55%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableViewModel.kt
index 0ed3203..821f212 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/GateBleTableViewModel.kt
@@ -1,6 +1,9 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view.table
+package llc.arma.ble.app.ui.screen.inspection.gate.table
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@@ -14,32 +17,33 @@ import llc.arma.ble.domain.usecase.GetHostBleTableBySerial
import javax.inject.Inject
@HiltViewModel
-class BleTableEditViewModel @Inject constructor(
- getFoundBle: GetFoundBle,
+class GateBleTableViewModel @Inject constructor(
+ private val getFoundBle: GetFoundBle,
+ private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow,
private val addBleToHostTable: AddBleToHostTable,
private val getHostBleTableBySerial: GetHostBleTableBySerial
-) : BaseViewModel() {
-
- private var lastSerial: String = ""
+) : BaseViewModel() {
init {
+ setEvent(GateBleTableContract.Event.OnRestart)
+
viewModelScope.launch {
while (true){
val state = viewState.value
- if(state is BleTableEditContract.State.Display) {
+ if(state is GateBleTableContract.State.Display) {
setState {
state.copy(bleAround = getFoundBle())
}
}
- delay(1_000)
+ delay(1_000)
}
@@ -47,49 +51,51 @@ class BleTableEditViewModel @Inject constructor(
}
- override fun setInitialState() = BleTableEditContract.State.Loading
+ override fun setInitialState() = GateBleTableContract.State.Loading
- override fun handleEvents(event: BleTableEditContract.Event) {
+ override fun handleEvents(event: GateBleTableContract.Event) {
when(event){
- is BleTableEditContract.Event.OnStart -> reduce(viewState.value, event)
- is BleTableEditContract.Event.OnAddBle -> reduce(viewState.value, event)
- is BleTableEditContract.Event.OnWritePreview -> reduce(viewState.value, event)
- is BleTableEditContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
- is BleTableEditContract.Event.OnWrite -> reduce(viewState.value, event)
+ is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
+ is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
+ is GateBleTableContract.Event.OnWritePreview -> reduce(viewState.value, event)
+ is GateBleTableContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
+ is GateBleTableContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
- state: BleTableEditContract.State,
- event: BleTableEditContract.Event.OnWrite
+ state: GateBleTableContract.State,
+ event: GateBleTableContract.Event.OnWrite
) {
- if(state is BleTableEditContract.State.Display) {
+ val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
+
+ if(state is GateBleTableContract.State.Display) {
viewModelScope.launch {
setState {
state.copy(
- writeState = BleTableEditContract.State.Display.WriteState.Writing(state.newTable)
+ writeState = GateBleTableContract.State.Display.WriteState.Writing(state.newTable)
)
}
addBleToHostTable.invoke(
- serial = lastSerial,
+ serial = params.bleSerial,
ble = state.newTable
).fold(
onSuccess = {
setState {
state.copy(
- writeState = BleTableEditContract.State.Display.WriteState.Success
+ writeState = GateBleTableContract.State.Display.WriteState.Success
)
}
- setEvent(BleTableEditContract.Event.OnStart(lastSerial))
+ setEvent(GateBleTableContract.Event.OnRestart)
},
onFailure = {
setState {
state.copy(
- writeState = BleTableEditContract.State.Display.WriteState.Failure
+ writeState = GateBleTableContract.State.Display.WriteState.Failure
)
}
}
@@ -102,11 +108,11 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
- state: BleTableEditContract.State,
- event: BleTableEditContract.Event.OnHideWritePreview
+ state: GateBleTableContract.State,
+ event: GateBleTableContract.Event.OnHideWritePreview
) {
- if(state is BleTableEditContract.State.Display) {
+ if(state is GateBleTableContract.State.Display) {
setState {
state.copy(writeState = null)
@@ -117,14 +123,17 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
- state: BleTableEditContract.State,
- event: BleTableEditContract.Event.OnWritePreview
+ state: GateBleTableContract.State,
+ event: GateBleTableContract.Event.OnWritePreview
) {
- if(state is BleTableEditContract.State.Display) {
+ if(state is GateBleTableContract.State.Display) {
setState {
- state.copy(writeState = BleTableEditContract.State.Display.WriteState.DisplayPreview(state.newTable))
+ state.copy(writeState = GateBleTableContract.State.Display.WriteState.DisplayPreview(
+ state.newTable
+ )
+ )
}
}
@@ -132,11 +141,11 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
- state: BleTableEditContract.State,
- event: BleTableEditContract.Event.OnAddBle
+ state: GateBleTableContract.State,
+ event: GateBleTableContract.Event.OnAddBle
) {
- if(state is BleTableEditContract.State.Display) {
+ if(state is GateBleTableContract.State.Display) {
if(state.newTable.any { it.serial == event.ble.serial}){
@@ -157,21 +166,22 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
- state: BleTableEditContract.State,
- event: BleTableEditContract.Event.OnStart
+ state: GateBleTableContract.State,
+ event: GateBleTableContract.Event.OnRestart
) {
- lastSerial = event.serial
+
+ val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState {
- BleTableEditContract.State.Loading
+ GateBleTableContract.State.Loading
}
viewModelScope.launch {
val names = getBleNamesFlow.invoke().first()
- getHostBleTableBySerial(event.serial).fold(
+ getHostBleTableBySerial(params.bleSerial).fold(
onSuccess = {
val savedBle = it.map { ble -> BleName(
@@ -179,17 +189,18 @@ class BleTableEditViewModel @Inject constructor(
serial = ble) }
setState {
- BleTableEditContract.State.Display(
+ GateBleTableContract.State.Display(
bleAround = emptyList(),
newTable = savedBle,
savedBleTable = savedBle,
- writeState = null)
+ writeState = null
+ )
}
},
onFailure = {
setState {
- BleTableEditContract.State.Error
+ GateBleTableContract.State.Error
}
}
)
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/Write.kt
similarity index 87%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/Write.kt
index fba0803..6da5292 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/gate/table/Write.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view.table
+package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@@ -24,11 +24,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
+import llc.arma.ble.app.ui.screen.ShapeType
@Composable
fun Write(
- state: BleTableEditContract.State.Display.WriteState,
- onEvent: (BleTableEditContract.Event) -> Unit
+ state: GateBleTableContract.State.Display.WriteState,
+ onEvent: (GateBleTableContract.Event) -> Unit
) {
Column(
@@ -44,7 +45,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp))
when (state) {
- is BleTableEditContract.State.Display.WriteState.DisplayPreview -> {
+ is GateBleTableContract.State.Display.WriteState.DisplayPreview -> {
Box(
modifier = Modifier
@@ -66,7 +67,7 @@ fun Write(
}
items(items = state.writeRequest) {
- SelectBleItem(it)
+ SelectBleItem(ShapeType.Singleton, it)
}
if(state.writeRequest.isEmpty()){
@@ -87,18 +88,18 @@ fun Write(
PrimaryButton(
label = "Записать"
) {
- onEvent(BleTableEditContract.Event.OnWrite)
+ onEvent(GateBleTableContract.Event.OnWrite)
}
SecondaryButton (
label = "Отменить"
) {
- onEvent(BleTableEditContract.Event.OnHideWritePreview)
+ onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
- is BleTableEditContract.State.Display.WriteState.Writing -> {
+ is GateBleTableContract.State.Display.WriteState.Writing -> {
Column {
@@ -115,13 +116,13 @@ fun Write(
SecondaryButton (
label = "Отменить"
) {
- onEvent(BleTableEditContract.Event.OnHideWritePreview)
+ onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
- BleTableEditContract.State.Display.WriteState.Success -> {
+ GateBleTableContract.State.Display.WriteState.Success -> {
Column {
@@ -153,13 +154,13 @@ fun Write(
PrimaryButton(
label = "Ok"
) {
- onEvent(BleTableEditContract.Event.OnHideWritePreview)
+ onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
- BleTableEditContract.State.Display.WriteState.Failure -> {
+ GateBleTableContract.State.Display.WriteState.Failure -> {
Column {
@@ -191,7 +192,7 @@ fun Write(
PrimaryButton(
label = "Ok"
) {
- onEvent(BleTableEditContract.Event.OnHideWritePreview)
+ onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt
deleted file mode 100644
index 197785e..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.host
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.hilt.navigation.compose.hiltViewModel
-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.TxLevelSelector
-import llc.arma.ble.app.ui.common.rememberBottomDialogState
-import llc.arma.ble.app.ui.screen.inspection.host.view.DisplayState
-import llc.arma.ble.app.ui.screen.inspection.host.view.IntervalEdit
-import llc.arma.ble.app.ui.screen.inspection.host.view.ReadIntervalEdit
-import llc.arma.ble.app.ui.screen.inspection.host.view.Write
-import llc.arma.ble.domain.model.Ble
-
-enum class SheetPage {
- WRITE, POWER_EDIT, INTERVAL_EDIT, READ_INTERVAL_EDIT
-}
-
-@Composable
-fun HostScreen(
- ble: Ble.Host,
- onNavigationEvent: (HostContract.Effect.Navigation) -> Unit
-) {
-
- val viewModel = hiltViewModel()
- val state = viewModel.viewState.value
-
- var sheetPage by rememberSaveable {
- mutableStateOf(null)
- }
-
- val bottomDialog = rememberBottomDialogState()
-
- LaunchedEffect("effect"){
- viewModel.effect.onEach {
- when(it){
- is HostContract.Effect.Navigation -> onNavigationEvent(it)
- HostContract.Effect.HideWriteBlePreview -> launch {
- sheetPage = null
- }
- HostContract.Effect.ShowWriteBlePreview -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.WRITE
- }
- HostContract.Effect.HidePowerPicker -> launch {
- sheetPage = null
- }
- HostContract.Effect.ShowPowerPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.POWER_EDIT
- }
-
- HostContract.Effect.HideIntervalPicker -> launch {
- sheetPage = null
- }
- HostContract.Effect.ShowIntervalPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.INTERVAL_EDIT
- }
-
- HostContract.Effect.HideReadIntervalPicker -> launch {
- sheetPage = null
- }
- HostContract.Effect.ShowReadIntervalPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.READ_INTERVAL_EDIT
- }
- }
- }.launchIn(this)
- }
-
- LaunchedEffect(ble){
- viewModel.setEvent(HostContract.Event.OnBleChanged(ble))
- }
-
- LaunchedEffect(sheetPage){
- when(sheetPage){
- SheetPage.WRITE -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is HostContract.State.Display && currentState.writeState != null) {
-
- Write(
- state = currentState.writeState,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
-
- }
-
- }
- SheetPage.POWER_EDIT -> bottomDialog.show {
- val currentState = viewModel.viewState.value
-
- if(currentState is HostContract.State.Display) {
- TxLevelSelector(
- tx = currentState.host.state.tx
- ) {
- viewModel.setEvent(HostContract.Event.OnPowerChanged(it))
- }
- }
- }
- SheetPage.INTERVAL_EDIT -> bottomDialog.show {
- val currentState = viewModel.viewState.value
-
- if(currentState is HostContract.State.Display) {
- IntervalEdit(
- state = currentState.host,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- }
- SheetPage.READ_INTERVAL_EDIT -> bottomDialog.show {
- val currentState = viewModel.viewState.value
-
- if(currentState is HostContract.State.Display) {
- ReadIntervalEdit(
- state = currentState.host,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- }
- else -> {
- bottomDialog.hide()
- }
- }
- }
-
- Column {
-
- when(state){
- is HostContract.State.Display -> DisplayState(
- onEvent = {
- viewModel.setEvent(it)
- },
- ble = state.host,
- origin = state.origin
- )
- is HostContract.State.Loading -> LoadingState()
- }
-
- }
-
-}
-
-@Composable
-private fun LoadingState(){
-
- Box(modifier = Modifier.fillMaxSize()){
-
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt
deleted file mode 100644
index f5e3111..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt
+++ /dev/null
@@ -1,313 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.host
-
-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.domain.model.Ble
-import llc.arma.ble.domain.usecase.WriteBle
-import javax.inject.Inject
-
-@HiltViewModel
-class HostViewModel @Inject constructor(
- private val bleMapper: BleMapper,
- private val writeBle: WriteBle,
- private val bleViewMapper: BleViewMapper
-) : BaseViewModel() {
-
- override fun setInitialState() = HostContract.State.Loading
-
- override fun handleEvents(event: HostContract.Event) {
- when(event){
- is HostContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
- is HostContract.Event.OnTxChanged -> reduce(viewState.value, event)
- is HostContract.Event.OnBleChanged -> reduce(viewState.value, event)
- is HostContract.Event.OnChangePassword -> reduce(viewState.value, event)
- is HostContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
- is HostContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
- is HostContract.Event.OnWriteBle -> reduce(viewState.value, event)
- is HostContract.Event.OnPowerChanged -> reduce(viewState.value, event)
- is HostContract.Event.OnPowerEdit -> reduce(viewState.value, event)
- is HostContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
- is HostContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
- is HostContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
- is HostContract.Event.OnShowIntervalEdit -> reduce(viewState.value, event)
- is HostContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event)
- is HostContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event)
- }
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnSaveReadIntervalChanged
- ) {
-
- if(state is HostContract.State.Display) {
-
- state.host.hostState.readInterval = event.interval
-
- }
-
- setEffect {
- HostContract.Effect.HideReadIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnShowReadIntervalEdit
- ) {
-
- setEffect {
- HostContract.Effect.ShowReadIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnSaveIntervalChanged
- ) {
-
- if(state is HostContract.State.Display) {
-
- state.host.hostState.historyInterval = event.interval
-
- }
-
- setEffect {
- HostContract.Effect.HideIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnShowIntervalEdit
- ) {
-
- setEffect {
- HostContract.Effect.ShowIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnShowHostBleTable
- ) {
-
- if(state is HostContract.State.Display) {
-
- setEffect {
- HostContract.Effect.Navigation.NavigateToBleTable(state.host.info.serial)
- }
-
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnShowHostHistory
- ) {
-
- if(state is HostContract.State.Display) {
-
- setEffect {
- HostContract.Effect.Navigation.NavigateToHostHistory(state.host.info)
- }
-
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnPowerChanged
- ) {
-
- if(state is HostContract.State.Display) {
-
- state.host.state.tx = event.tx
-
- }
-
- setEffect {
- HostContract.Effect.HidePowerPicker
- }
-
- }
-
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnPowerEdit
- ) {
- setEffect { HostContract.Effect.ShowPowerPicker }
- }
-
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnNavigateUpClicked
- ) {
- setEffect { HostContract.Effect.Navigation.NavigateUp }
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnTxChanged
- ) {
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnBleChanged
- ) {
-
- when(state){
- is HostContract.State.Display -> {
- setState {
- state.copy(
- origin = Ble.Host(
- info = event.ble.info,
- state = state.origin.state,
- hostState = state.origin.hostState
- )
- )
- }
- }
- is HostContract.State.Loading -> {
- setState {
- HostContract.State.Display(
- origin = event.ble,
- host = bleMapper.map(event.ble) as BleView.Host,
- writeState = null
- )
- }
- }
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnChangePassword
- ) {
- setEffect {
- HostContract.Effect.Navigation.NavigateToChangePassword
- }
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnHideWriteBlePreview
- ) {
- setEffect {
- HostContract.Effect.HideWriteBlePreview
- }
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnShowWriteBlePreview
- ) {
-
- if(state is HostContract.State.Display){
-
- val newBle = bleViewMapper.map(state.host) as Ble.Host
-
- val writeRequest = Ble.Host.WriteRequest(
- tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
- interval = if(newBle.hostState.historyInterval == state.origin.hostState.historyInterval) null else newBle.hostState.historyInterval,
- readInterval = if(newBle.hostState.readInterval == state.origin.hostState.readInterval) null else newBle.hostState.readInterval,
- )
-
- setState {
- state.copy(
- writeState = HostContract.State.Display.WriteState.DisplayPreview(
- writeRequest
- )
- )
- }
-
- setEffect {
- HostContract.Effect.ShowWriteBlePreview
- }
-
- }
-
- }
-
- private fun reduce(
- state: HostContract.State,
- event: HostContract.Event.OnWriteBle
- ) {
-
- if(state is HostContract.State.Display){
-
- state.writeState?.let { request ->
-
- if(request is HostContract.State.Display.WriteState.DisplayPreview) {
-
- viewModelScope.launch {
-
- setState {
- state.copy(
- writeState = HostContract.State.Display.WriteState.Writing(request.writeRequest)
- )
- }
-
- val currentState = viewState.value
-
- if(currentState is HostContract.State.Display) {
-
- val newBleObject = Ble.Host(
- info = currentState.origin.info,
- state = currentState.origin.state.copy(
- tx = request.writeRequest.tx ?: state.origin.state.tx
- ),
- hostState = currentState.origin.hostState.copy(
- historyInterval = request.writeRequest.interval
- ?: currentState.origin.hostState.historyInterval
- )
- )
-
- writeBle(state.host.info.serial, request.writeRequest).fold(
- onSuccess = {
- setState {
- currentState.copy(
- origin = newBleObject,
- host = bleMapper.map(newBleObject) as BleView.Host,
- writeState = HostContract.State.Display.WriteState.Success
- )
- }
- },
- onFailure = {
- setState {
- state.copy(
- writeState = HostContract.State.Display.WriteState.Failure
- )
- }
- }
- )
-
- }
-
- }
-
- }
-
- }
-
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt
deleted file mode 100644
index 3fc0000..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt
+++ /dev/null
@@ -1,198 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowRight
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.graphics.vector.rememberVectorPainter
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.BleInfoView
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
-import llc.arma.ble.domain.model.Ble
-
-@Composable
-fun DisplayState(
- onEvent: (HostContract.Event) -> Unit,
- origin: Ble.Host,
- ble: BleView.Host
-) {
-
- val scrollState = rememberScrollState()
-
- Column {
-
- Column(
- modifier = Modifier
- .verticalScroll(scrollState)
- .weight(1f)
- ) {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
- BleInfoView(
- bleInfo = origin.info,
- version = origin.state.version
- )
- }
-
- Column(
- modifier = Modifier,
- content = {
-
- BleMenuItem(
- title = "Мощность",
- subtitle = "${ble.state.tx.value} db",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
- onEvent(HostContract.Event.OnPowerEdit)
- }
-
- var hours =
- ble.hostState.historyInterval / millisInHour
- var minutes =
- (ble.hostState.historyInterval - (hours * millisInHour)) / millisInMinute
- var seconds =
- (ble.hostState.historyInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
-
- BleMenuItem(
- title = "Интервал измерений",
- subtitle = "$hours ч. $minutes мин. $seconds сек.",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
- onEvent(HostContract.Event.OnShowIntervalEdit)
- }
-
- hours = ble.hostState.readInterval / millisInHour
- minutes =
- (ble.hostState.readInterval - (hours * millisInHour)) / millisInMinute
- seconds =
- (ble.hostState.readInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
-
- BleMenuItem(
- title = "Интервал чтения",
- subtitle = "$hours ч. $minutes мин. $seconds сек.",
- icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
- ) {
- onEvent(HostContract.Event.OnShowReadIntervalEdit)
- }
-
- BleMenuItem(
- title = "График измерений",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
- onEvent(HostContract.Event.OnShowHostHistory)
- }
-
- BleMenuItem(
- title = "Таблица BLE ID",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
- onEvent(HostContract.Event.OnShowHostBleTable)
- }
-
- BleMenuItem(
- title = "Изменить пароль",
- icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
- ) {
- onEvent(HostContract.Event.OnChangePassword)
- }
-
- }
- )
-
- }
-
- PrimaryButton(
- modifier = Modifier.shadow(
- if(scrollState.canScrollForward){
- 8.dp
- } else {
- 0.dp
- }
- ).background(MaterialTheme.colorScheme.background),
- label = "Сохранить"
- ) {
- onEvent(HostContract.Event.OnShowWriteBlePreview)
- }
-
- }
-
-}
-
-@Composable
-fun BleMenuItem(
- title: String,
- subtitle: String? = null,
- icon: Painter? = null,
- onClick: (() -> Unit) = {}
-){
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .clickable(onClick = onClick)
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = title
- )
-
- subtitle?.let {
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = it
- )
- }
-
- }
-
- if (icon != null) {
- Icon(
- painter = icon,
- contentDescription = null
- )
- }
-
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt
deleted file mode 100644
index e5b3a50..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt
+++ /dev/null
@@ -1,705 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view
-
-import android.content.res.Configuration
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ExperimentalLayoutApi
-import androidx.compose.foundation.layout.FlowColumn
-import androidx.compose.foundation.layout.FlowRow
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.ContentAlpha
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.ArrowBack
-import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material.icons.rounded.Save
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FilterChip
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.VerticalDivider
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.viewModelScope
-import com.patrykandpatrick.vico.compose.axis.axisLineComponent
-import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
-import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
-import com.patrykandpatrick.vico.compose.chart.Chart
-import com.patrykandpatrick.vico.compose.chart.column.columnChart
-import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
-import com.patrykandpatrick.vico.compose.component.shapeComponent
-import com.patrykandpatrick.vico.compose.component.textComponent
-import com.patrykandpatrick.vico.core.axis.AxisPosition
-import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
-import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
-import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
-import com.patrykandpatrick.vico.core.component.shape.LineComponent
-import com.patrykandpatrick.vico.core.component.shape.Shapes.pillShape
-import com.patrykandpatrick.vico.core.dimensions.MutableDimensions
-import com.patrykandpatrick.vico.core.entry.ChartEntry
-import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
-import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer
-import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
-import com.patrykandpatrick.vico.core.scroll.InitialScroll
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.first
-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.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.common.ProgressState
-import llc.arma.ble.domain.model.Ble
-import llc.arma.ble.domain.model.BleInfo
-import llc.arma.ble.domain.model.BleName
-import llc.arma.ble.domain.usecase.ExportToXlsx
-import llc.arma.ble.domain.usecase.GetBleBySerial
-import llc.arma.ble.domain.usecase.GetBleNamesFlow
-import llc.arma.ble.domain.usecase.GetHostHistoryBySerial
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import javax.inject.Inject
-
-class HostEntry(
- val localDate: Long,
- override val x: Float,
- override val y: Float,
-) : ChartEntry {
-
- override fun withY(y: Float) = HostEntry(localDate, x, y)
-
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun HostHistory(
- ble: BleInfo,
- onDismiss: (() -> Unit)? = null,
-) {
-
- val viewModel = hiltViewModel()
- val state = viewModel.viewState.value
-
- LaunchedEffect(ble.serial) {
- viewModel.setEvent(HostHistoryContract.Event.OnStart(ble.name, ble.serial))
- }
-
- Column {
-
- TopAppBar(
- navigationIcon = {
- onDismiss?.let {
-
- IconButton(onClick = it) {
- Icon(
- imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
- contentDescription = null
- )
- }
-
- }
- },
- title = {
- val title = when(state){
- is HostHistoryContract.State.Display -> {
- when (state.loadingHistoryState) {
- is ProgressState.Finished -> "Таблица (${state.loadingHistoryState.data.size})"
- is ProgressState.Indeterminate -> "Таблица"
- is ProgressState.Progress -> "Таблица"
- }
- }
- HostHistoryContract.State.Exception -> "Таблица"
- }
-
- Text(
- modifier = Modifier.weight(1f),
- text = title,
- style = MaterialTheme.typography.titleLarge
- )
- },
- actions = {
-
- IconButton(
- onClick = {
- viewModel.setEvent(HostHistoryContract.Event.OnExportHistory(ble.serial))
- },
- enabled = when(state){
- is HostHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
- HostHistoryContract.State.Exception -> true
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Save,
- contentDescription = null
- )
- }
-
- IconButton(
- onClick = {
- viewModel.setEvent(HostHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial))
- },
- enabled = when(state){
- is HostHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
- HostHistoryContract.State.Exception -> true
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Refresh,
- contentDescription = null
- )
- }
- }
- )
-
- Box(modifier = Modifier) {
-
- when (state) {
- is HostHistoryContract.State.Display -> Display(state = state)
- is HostHistoryContract.State.Exception -> Exception()
- }
-
- }
-
- }
-
-}
-
-val dayFormatter = SimpleDateFormat("dd", Locale.getDefault())
-val dateFormatter = SimpleDateFormat("dd.MM", Locale.getDefault())
-val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
-
-val colorsStack = listOf(
- Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
- Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
- Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
- Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
- Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
- Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
- Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
- Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
- Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
- Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
- Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
- Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
- Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
- Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
- Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
- Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
-)
-
-val startAxisValueFormatter =
- AxisValueFormatter { value, chartValues ->
- " "
- }
-
-val axisValueFormatter =
- AxisValueFormatter { value, chartValues ->
- val first = (chartValues.chartEntryModel.entries.firstOrNull()?.firstOrNull() as? HostEntry)
- val last = (chartValues.chartEntryModel.entries.firstOrNull()?.lastOrNull() as? HostEntry)
- val previous = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt() - 1) as? HostEntry)
- val current = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt()) as? HostEntry)
-
- if(current != null) {
- if (first == current || last == current) {
- dateFormatter.format(Date(current.localDate))
- } else {
- if(previous != null && dayFormatter.format(previous.localDate) != dayFormatter.format(current.localDate)){
- dateFormatter.format(Date(current.localDate))
- }else{
- timeFormatter.format(Date(current.localDate))
- }
- }.orEmpty()
- } else {
- " "
- }
- }
-
-@OptIn(ExperimentalLayoutApi::class)
-@Composable
-fun Display(
- state: HostHistoryContract.State.Display
-) {
-
- val configuration = LocalConfiguration.current
-
- 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 allSerials = remember(state) { state.loadingHistoryState.data.flatMap { it.value }.distinct() }
-
- val colors = remember(allSerials) {
- allSerials.mapIndexed { index, s ->
- Pair(s, colorsStack[index])
- }.toMap()
- }
-
- var selectedSerials by rememberSaveable {
- mutableStateOf(allSerials.take(1))
- }
-
- val serials = remember(selectedSerials) { allSerials.filter { selectedSerials.contains(it) } }
-
- val entries = remember(serials, state) {
-
- serials.map { serial ->
-
- ChartEntryModelProducer(
- state.loadingHistoryState.data.mapIndexed { index, historyPoint ->
- if(historyPoint.value.contains(serial)) {
- HostEntry(historyPoint.date, index.toFloat(), 1f)
- } else {
- HostEntry(historyPoint.date, index.toFloat(), 0.0001f)
- }
- }
- )
-
- }
-
- }
-
- val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
-
- val chart = columnChart(
- persistentMarkers = state.loadingHistoryState.data.mapIndexedNotNull { index, historyPoint ->
- if(historyPoint.hit) {
- Pair(
- index.toFloat(),
- MarkerComponent(
- label = textComponent(
- textSize = 0.sp,
- padding = MutableDimensions(10f, 10f, 10f, 10f),
- margins = MutableDimensions(10f, 10f, 10f, 10f),
- color = Color.Red,
- background = shapeComponent(
- color = Color.Red,
- shape = pillShape
- )
- ),
- indicator = null,
- guideline = axisLineComponent(
- thickness = 0.dp
- )
- )
- )
- }else{
- null
- }
- }.toMap(),
- innerSpacing = 2.dp,
- columns = serials.map { LineComponent(color = colors[it]!!.toArgb(), thicknessDp = 7f, shape= pillShape) },
- spacing = 8.dp,
- )
-
- @Composable
- fun LegendItem(s: String) {
-
- FilterChip(
- selected = selectedSerials.contains(s),
- onClick = {
- selectedSerials = if(selectedSerials.contains(s)){
- selectedSerials.toMutableList().apply {
- remove(s)
- }
- }else{
- selectedSerials.toMutableList().apply {
- add(s)
- }
- }
- },
- leadingIcon = {
- Surface(
- shape = CircleShape,
- color = colors[s]!!,
- modifier = Modifier.size(28.dp)
- ) {}
- },
- label = { Column {
- Text(text = state.bleNames.firstOrNull { it.serial == s }?.name ?: s)
- Text(
- style = MaterialTheme.typography.bodySmall.copy(
- color = LocalTextStyle.current.color.copy(
- alpha = ContentAlpha.medium
- )
- ),
- text = s
- )
- }}
- )
-
- }
-
- when (configuration.orientation) {
- Configuration.ORIENTATION_LANDSCAPE -> {
- Row(
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
-
- Chart(
- chart = chart,
- chartModelProducer = producer,
- bottomAxis = bottomAxis(
- labelRotationDegrees = -90f,
- valueFormatter = axisValueFormatter,
- tickLength = 0.dp,
- ),
- startAxis = startAxis(
- valueFormatter = startAxisValueFormatter
- ),
- modifier = Modifier
- .fillMaxHeight()
- .weight(1f),
- autoScaleUp = AutoScaleUp.None,
- diffAnimationSpec = tween(0),
- chartScrollSpec = rememberChartScrollSpec(
- initialScroll = InitialScroll.End,
- autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
- autoScrollAnimationSpec = tween(0)
- )
- )
-
- VerticalDivider()
-
- FlowRow(
- maxItemsInEachRow = 1,
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.verticalScroll(rememberScrollState())
- ) {
-
- allSerials.mapIndexed { _, s ->
- LegendItem(s = s)
- }
-
- }
-
- }
- }
- else -> {
- Column(
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
-
- FlowColumn(
- maxItemsInEachColumn = 2,
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.horizontalScroll(rememberScrollState())
- ) {
-
- allSerials.mapIndexed { _, s ->
- LegendItem(s = s)
- }
-
- }
-
- HorizontalDivider()
-
- Chart(
-
- chart = chart,
- chartModelProducer = producer,
- bottomAxis = bottomAxis(
- labelRotationDegrees = -90f,
- valueFormatter = axisValueFormatter,
- tickLength = 0.dp,
- ),
- 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 HostHistoryContract {
-
- sealed class Event : ViewEvent {
-
- data object StopMeasure : Event()
-
- data class OnStart(
- val bleName: String,
- val serial: String,
- ) : Event()
-
- data class OnRefreshHistory(
- val bleName: String,
- val serial: String,
- ) : Event()
-
- data class OnExportHistory(
- val serial: String,
- ) : Event()
-
- }
-
- sealed class State : ViewState {
-
- data class Display(
- val bleName: String,
- val bleNames: List,
- val loadingHistoryState : ProgressState>
- ) : State()
-
- data object Exception : State()
-
- }
-
- sealed class Effect : ViewSideEffect {
-
- }
-
-}
-
-
-
-@HiltViewModel
-class HostHistoryViewModel @Inject constructor(
- private val getHostHistoryBySerial: GetHostHistoryBySerial,
- private val getBleNamesFlow: GetBleNamesFlow,
- private val exportToXlsx: ExportToXlsx
-) : BaseViewModel() {
-
- private var measureJob: Job? = null
-
- private var lastSerial: String? = null
-
- override fun setInitialState() = HostHistoryContract.State.Display(
- "",
- emptyList(),
- ProgressState.Indeterminate
- )
-
- override fun handleEvents(event: HostHistoryContract.Event) {
- when(event){
- is HostHistoryContract.Event.OnStart -> reduce(viewState.value, event)
- is HostHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
- is HostHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
- is HostHistoryContract.Event.OnExportHistory -> reduce(viewState.value, event)
- }
- }
-
- private fun reduce(
- state: HostHistoryContract.State,
- event: HostHistoryContract.Event.OnExportHistory
- ) {
-
- if(state is HostHistoryContract.State.Display) {
-
- if(state.loadingHistoryState is ProgressState.Finished) {
-
- exportToXlsx.invoke(event.serial, state.loadingHistoryState.data)
-
- }
-
- }
-
- }
-
- private fun reduce(
- state: HostHistoryContract.State,
- event: HostHistoryContract.Event.StopMeasure
- ) {
-
- measureJob?.cancel()
- measureJob = null
-
- setState {
- HostHistoryContract.State.Exception
- }
-
- }
-
- private fun reduce(
- state: HostHistoryContract.State,
- event: HostHistoryContract.Event.OnStart
- ) {
-
- viewModelScope.launch {
-
- if(state is HostHistoryContract.State.Display) {
-
- if(lastSerial != event.serial) {
-
- lastSerial = event.serial
-
- setState {
- HostHistoryContract.State.Display(event.bleName, emptyList(), ProgressState.Indeterminate)
- }
-
- measureJob?.cancel()
- measureJob = null
-
- val names = getBleNamesFlow().first()
-
- measureJob = getHostHistoryBySerial(event.serial).onEach {
- it.fold(
- onSuccess = {
- setState {
- HostHistoryContract.State.Display(event.bleName, names, it)
- }
- },
- onFailure = {
- setState {
- HostHistoryContract.State.Exception
- }
- }
- )
- }.launchIn(this)
-
- }
-
- }
-
- }
-
- }
-
- private fun reduce(
- state: HostHistoryContract.State,
- event: HostHistoryContract.Event.OnRefreshHistory
- ) {
- viewModelScope.launch {
-
- setState {
-
- HostHistoryContract.State.Display("", emptyList(), ProgressState.Indeterminate)
- }
-
- measureJob?.cancel()
- measureJob = null
-
- val names = getBleNamesFlow().first()
-
- measureJob = getHostHistoryBySerial(event.serial).onEach {
- it.fold(
- onSuccess = {
- setState {
- HostHistoryContract.State.Display(event.bleName, names, it)
- }
- },
- onFailure = {
- setState {
- HostHistoryContract.State.Exception
- }
- }
- )
- }.launchIn(this)
-
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/ReadIntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/ReadIntervalEdit.kt
deleted file mode 100644
index 1cd0184..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/ReadIntervalEdit.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
-
-@Composable
-fun ReadIntervalEdit(
- state: BleView.Host,
- onEvent: (HostContract.Event) -> Unit,
-){
-
- var value by remember(state.hostState.readInterval) {
- mutableIntStateOf((state.hostState.readInterval).toInt())
- }
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Интервал чтения",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- DurationPicker(
- modifier = Modifier.align(Alignment.CenterHorizontally),
- value = value
- ) {
- value = it
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(
- HostContract.Event.OnSaveReadIntervalChanged(
- value.toLong()
- )
- )
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorContract.kt
new file mode 100644
index 0000000..39e254a
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorContract.kt
@@ -0,0 +1,37 @@
+package llc.arma.ble.app.ui.screen.inspection.selector.duration
+
+import llc.arma.ble.app.ui.common.ViewEvent
+import llc.arma.ble.app.ui.common.ViewSideEffect
+import llc.arma.ble.app.ui.common.ViewState
+
+class DurationSelectorContract {
+
+ sealed class Event : ViewEvent {
+
+ data class OnDurationChanged(
+ val duration: Int
+ ) : Event()
+
+ data object OnSave : Event()
+
+ }
+
+ data class State(
+ val duration: Int
+ ) : ViewState
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data object Up : Navigation()
+
+ data class UpWithResult(
+ val duration: Int
+ ) : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorScreen.kt
similarity index 73%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorScreen.kt
index 5e694a7..f2c98a2 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorScreen.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.host.view
+package llc.arma.ble.app.ui.screen.inspection.selector.duration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
@@ -10,15 +10,21 @@ import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
+import androidx.compose.material3.Button
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -29,22 +35,54 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
+import com.ramcosta.composedestinations.result.ResultBackNavigator
+import com.ramcosta.composedestinations.spec.DestinationStyle
+import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.inspection.host.HostContract
+import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
+@Serializable
+data class DurationSelectResult(
+ val qualifier: String?,
+ val duration: Int,
+)
+
+@Destination(style = DestinationStyleBottomSheet::class)
@Composable
-fun IntervalEdit(
- state: BleView.Host,
- onEvent: (HostContract.Event) -> Unit,
-){
+fun DurationSelectorScreen(
+ qualifier: String? = null,
+ duration: Int?,
+ minimum: Int = 10_000,
+ maximum: Int = 10 * 24 * 60 * 60 * 1000,
+ daysComponent: Boolean = true,
+ hoursComponent: Boolean = true,
+ minutesComponent: Boolean = true,
+ secondsComponent: Boolean = true,
+ resultNavigator: ResultBackNavigator
+) {
- var value by remember(state.hostState.historyInterval) {
- mutableIntStateOf((state.hostState.historyInterval).toInt())
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect {
+ when(it){
+ DurationSelectorContract.Effect.Navigation.Up ->
+ resultNavigator.navigateBack()
+
+ is DurationSelectorContract.Effect.Navigation.UpWithResult ->
+ resultNavigator.navigateBack(DurationSelectResult(qualifier, it.duration))
+ }
+ }
}
Column(
- modifier = Modifier
+ modifier = Modifier.padding(16.dp).fillMaxWidth()
) {
Text(
@@ -56,22 +94,30 @@ fun IntervalEdit(
Spacer(modifier = Modifier.height(16.dp))
DurationPicker(
- modifier = Modifier.align(Alignment.CenterHorizontally),
- value = value
+ minInterval = minimum,
+ maxInterval = maximum,
+ days = daysComponent,
+ hours = hoursComponent,
+ minutes = minutesComponent,
+ seconds = secondsComponent,
+ value = state.duration,
+ modifier = Modifier.align(Alignment.CenterHorizontally)
) {
- value = it
+ viewModel.setEvent(DurationSelectorContract.Event.OnDurationChanged(it))
}
Spacer(modifier = Modifier.height(16.dp))
- PrimaryButton(
- label = "Применить"
+ Button(
+ onClick = {
+ viewModel.setEvent(DurationSelectorContract.Event.OnSave)
+ },
+ modifier = Modifier.fillMaxWidth()
) {
- onEvent(
- HostContract.Event.OnSaveIntervalChanged(
- value.toLong()
- )
+ Text(
+ text = "Применить"
)
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorViewModel.kt
new file mode 100644
index 0000000..09bb972
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/duration/DurationSelectorViewModel.kt
@@ -0,0 +1,52 @@
+package llc.arma.ble.app.ui.screen.inspection.selector.duration
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import llc.arma.ble.app.ui.common.BaseViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class DurationSelectorViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle
+) : BaseViewModel() {
+
+ init {
+
+ val params = DurationSelectorScreenDestination.argsFrom(savedStateHandle)
+
+ setState { DurationSelectorContract.State(params.duration ?: 0) }
+ }
+
+ override fun setInitialState() = DurationSelectorContract.State(0)
+
+ override fun handleEvents(event: DurationSelectorContract.Event) {
+ when(event){
+ is DurationSelectorContract.Event.OnDurationChanged -> reduce(viewState.value, event)
+ is DurationSelectorContract.Event.OnSave -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: DurationSelectorContract.State,
+ event: DurationSelectorContract.Event.OnDurationChanged,
+ ) {
+
+ setState { DurationSelectorContract.State(event.duration) }
+
+ }
+
+ private fun reduce(
+ state: DurationSelectorContract.State,
+ event: DurationSelectorContract.Event.OnSave,
+ ) {
+
+ setEffect {
+ DurationSelectorContract.Effect.Navigation.UpWithResult(state.duration)
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorContract.kt
new file mode 100644
index 0000000..8272cfa
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorContract.kt
@@ -0,0 +1,40 @@
+package llc.arma.ble.app.ui.screen.inspection.selector.power
+
+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
+
+class TxPowerSelectorContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnNavigateUp : Event()
+
+ data class OnSelected(
+ val tx: BleView.BleState.TX
+ ) : Event()
+
+ data object OnSave : Event()
+
+ }
+
+ data class State(
+ val tx: BleView.BleState.TX?
+ ) : ViewState
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data object NavigateUp : Navigation()
+
+ data class NavigateUpWithResult(
+ val tx: BleView.BleState.TX
+ ) : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorScreen.kt
new file mode 100644
index 0000000..73c9cd5
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorScreen.kt
@@ -0,0 +1,79 @@
+package llc.arma.ble.app.ui.screen.inspection.selector.power
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultBackNavigator
+import llc.arma.ble.app.ui.common.PrimaryButton
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.SelectorItem
+
+@Destination(style = DestinationStyleBottomSheet::class)
+@Composable
+fun TxPowerSelectorScreen(
+ tx: BleView.BleState.TX?,
+ resultNavigator: ResultBackNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect {
+ when(it){
+ TxPowerSelectorContract.Effect.Navigation.NavigateUp ->
+ resultNavigator.navigateBack()
+ is TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult ->
+ resultNavigator.navigateBack(it.tx)
+ }
+ }
+ }
+
+ Column {
+
+ Text(
+ text = "Мощность",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ BleView.BleState.TX.entries.forEach {
+
+ SelectorItem(
+ label = "${it.value} dBb (${it.powerPercentage} %)",
+ selected = it == state.tx
+ ){
+ viewModel.setEvent(TxPowerSelectorContract.Event.OnSelected(it))
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ PrimaryButton(
+ label = "Применить"
+ ) {
+ viewModel.setEvent(TxPowerSelectorContract.Event.OnSave)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorViewModel.kt
new file mode 100644
index 0000000..65eaa70
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/selector/power/TxPowerSelectorViewModel.kt
@@ -0,0 +1,70 @@
+package llc.arma.ble.app.ui.screen.inspection.selector.power
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import llc.arma.ble.app.ui.common.BaseViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class TxPowerSelectorViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle
+) : BaseViewModel() {
+
+ init {
+
+ val params = TxPowerSelectorScreenDestination.argsFrom(savedStateHandle)
+
+ setState { TxPowerSelectorContract.State(params.tx) }
+
+ }
+
+ override fun setInitialState() = TxPowerSelectorContract.State(null)
+
+ override fun handleEvents(event: TxPowerSelectorContract.Event) {
+ when(event){
+ is TxPowerSelectorContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ is TxPowerSelectorContract.Event.OnSave -> reduce(viewState.value, event)
+ is TxPowerSelectorContract.Event.OnSelected -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: TxPowerSelectorContract.State,
+ event: TxPowerSelectorContract.Event.OnNavigateUp
+ ){
+
+ setEffect {
+ TxPowerSelectorContract.Effect.Navigation.NavigateUp
+ }
+
+ }
+
+ private fun reduce(
+ state: TxPowerSelectorContract.State,
+ event: TxPowerSelectorContract.Event.OnSave
+ ){
+
+ if(state.tx != null) {
+
+ setEffect {
+ TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult(state.tx)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: TxPowerSelectorContract.State,
+ event: TxPowerSelectorContract.Event.OnSelected
+ ){
+
+ setState {
+ TxPowerSelectorContract.State(event.tx)
+ }
+
+ }
+
+}
\ No newline at end of file
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
deleted file mode 100644
index b57568c..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerScreen.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.hilt.navigation.compose.hiltViewModel
-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.TxLevelSelector
-import llc.arma.ble.app.ui.common.rememberBottomDialogState
-import llc.arma.ble.app.ui.screen.inspection.thermometer.view.DisplayState
-import llc.arma.ble.app.ui.screen.inspection.thermometer.view.IntervalEdit
-import llc.arma.ble.app.ui.screen.inspection.thermometer.view.LoadingState
-import llc.arma.ble.app.ui.screen.inspection.thermometer.view.TemperatureHistory
-import llc.arma.ble.app.ui.screen.inspection.thermometer.view.Write
-import llc.arma.ble.domain.model.Ble
-
-enum class SheetPage {
- INTERVAL, POWER, TEMPERATURE_HISTORY, WRITE
-}
-
-val Boolean.localizedName: String
- get() {
- return if(this){
- "Включено"
- } else {
- "Выключено"
- }
- }
-
-@Composable
-fun ThermometerScreen(
- ble: Ble.Thermometer,
- onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
-) {
-
- var sheetPage by rememberSaveable {
- mutableStateOf(null)
- }
-
- val viewModel = hiltViewModel()
- val state = viewModel.viewState.value
-
- val bottomDialog = rememberBottomDialogState()
-
- LaunchedEffect(sheetPage){
- when(sheetPage){
- SheetPage.INTERVAL -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is ThermometerContract.State.Display) {
- IntervalEdit(
- state = currentState.thermometer,
- onEvent = viewModel::setEvent
- )
- }
-
- }
- SheetPage.POWER -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if(currentState is ThermometerContract.State.Display) {
-
- TxLevelSelector(
- tx = currentState.thermometer.state.tx
- ) {
- viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
- }
-
- }
-
- }
- SheetPage.TEMPERATURE_HISTORY -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if (currentState is ThermometerContract.State.Display) {
- TemperatureHistory(
- ble = currentState.thermometer.info
- )
- }
- }
- SheetPage.WRITE -> bottomDialog.show {
-
- val currentState = viewModel.viewState.value
-
- if (currentState is ThermometerContract.State.Display) {
-
- currentState.writeState?.let {
-
- Write(
- state = it,
- onEvent = viewModel::setEvent
- )
-
- }
-
- }
- }
- else -> {
- bottomDialog.hide()
- }
- }
- }
-
- LaunchedEffect("effect"){
- viewModel.effect.onEach {
- when(it){
- is ThermometerContract.Effect.Navigation -> {
- sheetPage = null
- onNavigationEvent(it)
- }
- is ThermometerContract.Effect.HideIntervalPicker -> launch {
- sheetPage = null
- delay(100)
- }
- is ThermometerContract.Effect.ShowIntervalPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.INTERVAL
- }
- is ThermometerContract.Effect.HidePowerPicker -> launch {
- sheetPage = null
- delay(100)
- }
- is ThermometerContract.Effect.ShowPowerPicker -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.POWER
- }
- is ThermometerContract.Effect.HideTemperatureHistory -> launch {
- sheetPage = null
- delay(100)
- }
- is ThermometerContract.Effect.ShowTemperatureHistory -> launch {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.TEMPERATURE_HISTORY
- }
- is ThermometerContract.Effect.HideWriteBle -> {
- sheetPage = null
- delay(100)
- }
- is ThermometerContract.Effect.ShowWriteBle -> {
- sheetPage = null
- delay(100)
- sheetPage = SheetPage.WRITE
- }
- }
- }.launchIn(this)
-
- }
-
- LaunchedEffect(ble){
- viewModel.setEvent(ThermometerContract.Event.OnBleChanged(ble))
- }
-
- Column {
-
- when(state){
- is ThermometerContract.State.Display -> {
- DisplayState(
- origin = state.origin,
- ble = state.thermometer,
- onEvent = {
- viewModel.setEvent(it)
- }
- )
- }
- is ThermometerContract.State.Loading -> LoadingState()
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryContract.kt
new file mode 100644
index 0000000..14e330f
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryContract.kt
@@ -0,0 +1,39 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.history
+
+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.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+
+class ThermometerHistoryContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnNavigateUp : Event()
+
+ data object OnRefresh : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data class Display(
+ val loadingHistoryState : ProgressState>
+ ) : State()
+
+ data object Exception : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data object Up : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryScreen.kt
new file mode 100644
index 0000000..bc9c2bd
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryScreen.kt
@@ -0,0 +1,264 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.history
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Refresh
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LoadingIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
+import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
+import com.patrykandpatrick.vico.compose.chart.Chart
+import com.patrykandpatrick.vico.compose.chart.line.lineChart
+import com.patrykandpatrick.vico.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.ChartEntryModelProducer
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import llc.arma.ble.domain.common.ProgressState
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class TemperatureEntry(
+ val localDate: Long,
+ override val x: Float,
+ override val y: Float,
+) : ChartEntry {
+
+ override fun withY(y: Float) = TemperatureEntry(localDate, x, y)
+
+}
+
+val formatter = SimpleDateFormat("dd.MM HH:mm", Locale.getDefault())
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ThermometerHistoryScreen(
+ bleSerial: String,
+ navigator: DestinationsNavigator
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect {
+ when(it){
+ ThermometerHistoryContract.Effect.Navigation.Up ->
+ navigator.popBackStack()
+ }
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(ThermometerHistoryContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ val title = when(state){
+ is ThermometerHistoryContract.State.Display -> {
+ when (state.loadingHistoryState) {
+ is ProgressState.Finished -> "История (${state.loadingHistoryState.data.size})"
+ is ProgressState.Indeterminate -> "История"
+ is ProgressState.Progress -> "История"
+ }
+ }
+ ThermometerHistoryContract.State.Exception -> "История"
+ }
+
+ Text(
+ modifier = Modifier,
+ text = title,
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ actions = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(ThermometerHistoryContract.Event.OnRefresh)
+ },
+ enabled = when(state){
+ is ThermometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
+ ThermometerHistoryContract.State.Exception -> true
+ }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Refresh,
+ contentDescription = null
+ )
+ }
+ }
+ )
+ }
+ ) {
+
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when (state) {
+ is ThermometerHistoryContract.State.Display -> DisplayState(state = state)
+ ThermometerHistoryContract.State.Exception -> ExceptionState()
+ }
+
+ }
+
+ }
+
+}
+
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun DisplayState(
+ state: ThermometerHistoryContract.State.Display
+) {
+
+ Box(modifier = Modifier
+ .padding(8.dp)
+ .fillMaxSize()
+ ) {
+
+ when (state.loadingHistoryState) {
+
+ is ProgressState.Finished -> {
+
+ if(state.loadingHistoryState.data.isEmpty()){
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ text = "Нет данных"
+ )
+
+ } else {
+
+ val producer = remember(state.loadingHistoryState.data) {
+ state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
+ TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
+ }.let {
+ ChartEntryModelProducer(it)
+ }
+ }
+
+ val axisValueFormatter =
+ AxisValueFormatter { value, chartValues ->
+ (chartValues.chartEntryModel.entries.firstOrNull()
+ ?.getOrNull(value.toInt()) as? TemperatureEntry)
+ ?.localDate
+ ?.let { formatter.format(Date(it)) }
+ .orEmpty()
+ }
+
+ val lineChart = lineChart()
+
+ val scrollState = rememberChartScrollState()
+
+ Chart(
+ chartScrollState = scrollState,
+ chart = lineChart,
+ chartModelProducer = producer,
+ startAxis = startAxis(),
+ bottomAxis = bottomAxis(
+ tickLength = 0.dp,
+ valueFormatter = axisValueFormatter,
+ labelRotationDegrees = -90f,
+ ),
+ modifier = Modifier.fillMaxSize(),
+ )
+
+ LaunchedEffect(scrollState.maxValue) {
+ scrollState.scrollBy(scrollState.maxValue)
+ }
+
+ }
+
+ }
+ is ProgressState.Indeterminate -> {
+
+ ContainedLoadingIndicator(
+ 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 = ""
+ )
+
+ LoadingIndicator(
+ progress = { progressAnimation },
+ modifier = Modifier.align(Alignment.Center)
+ )
+
+ }
+
+ }
+
+ }
+
+}
+
+@Composable
+private fun ExceptionState() {
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ .aspectRatio(2f),
+ ){
+
+ Text(
+ textAlign = TextAlign.Center,
+ text = "Во время загрузки произошла ошибка",
+ modifier = Modifier.align(Alignment.Center)
+ )
+
+ }
+
+}
+
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryViewModel.kt
new file mode 100644
index 0000000..823a926
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/history/ThermometerHistoryViewModel.kt
@@ -0,0 +1,83 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.history
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
+import javax.inject.Inject
+
+@HiltViewModel
+class ThermometerHistoryViewModel @Inject constructor(
+ private val savedStateHandle: SavedStateHandle,
+ private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
+) : BaseViewModel() {
+
+ init {
+
+ viewModelScope.launch { refresh() }
+
+ }
+
+ override fun setInitialState() = ThermometerHistoryContract.State.Display(
+ ProgressState.Indeterminate
+ )
+
+ override fun handleEvents(event: ThermometerHistoryContract.Event) {
+ when(event){
+ is ThermometerHistoryContract.Event.OnRefresh -> reduce(viewState.value, event)
+ is ThermometerHistoryContract.Event.OnNavigateUp -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: ThermometerHistoryContract.State,
+ event: ThermometerHistoryContract.Event.OnRefresh
+ ) {
+
+ viewModelScope.launch { refresh() }
+
+ }
+
+ private fun reduce(
+ state: ThermometerHistoryContract.State,
+ event: ThermometerHistoryContract.Event.OnNavigateUp
+ ) {
+
+ setEffect {
+ ThermometerHistoryContract.Effect.Navigation.Up
+ }
+
+ }
+
+ private suspend fun refresh() {
+
+ val params = ThermometerHistoryScreenDestination.argsFrom(savedStateHandle)
+
+ setState {
+ ThermometerHistoryContract.State.Display(ProgressState.Indeterminate)
+ }
+
+ getTemperatureHistoryBySerial(params.bleSerial).collect {
+ it.fold(
+ onSuccess = {
+ setState {
+ ThermometerHistoryContract.State.Display(it)
+ }
+ },
+ onFailure = {
+ setState {
+ ThermometerHistoryContract.State.Exception
+ }
+ }
+ )
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/DisplayState.kt
new file mode 100644
index 0000000..14d92d1
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/DisplayState.kt
@@ -0,0 +1,196 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.common.PrimaryButton
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.BleInfoView
+import llc.arma.ble.app.ui.screen.ShapeType
+import llc.arma.ble.domain.model.Ble
+
+@Composable
+fun DisplayState(
+ viewModel: ThermometerViewModel,
+ state: ThermometerContract.State.Display
+) {
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(horizontal = 16.dp)
+ ) {
+
+ BleInfoView(
+ bleInfo = state.origin.info,
+ version = state.origin.state.version
+ )
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ content = {
+
+ BleMenuItem(
+ shapeType = ShapeType.Start,
+ title = "Мощность",
+ subtitle = "${state.thermometer.state.tx.value} db",
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnTxSelect)
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ )
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Температура",
+ subtitle = "${state.thermometer.thermometerState.temperature.value} °C",
+ onClick = {
+ //onEvent(ThermometerContract.Event.OnPowerEdit)
+ }
+ )
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Сохранять измерения",
+ onClick = {
+ //onEvent(ThermometerContract.Event.OnPowerEdit)
+ },
+ icon = {
+ Switch(
+ checked = state.thermometer.thermometerState.saveHistory,
+ onCheckedChange = {
+ viewModel.setEvent(ThermometerContract.Event.OnSaveHistoryChanged(it))
+ }
+ )
+ }
+ )
+
+ val hours = state.thermometer.thermometerState.historyInterval / 1000 / 60 / 60
+ val minutes =
+ (state.thermometer.thermometerState.historyInterval - (hours * 1000 * 60 * 60)) / 1000 / 60
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "Интервал измерений",
+ subtitle = "$hours ч. $minutes мин.",
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalEdit)
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.KeyboardArrowDown,
+ contentDescription = null
+ )
+ }
+ )
+
+ BleMenuItem(
+ shapeType = ShapeType.Middle,
+ title = "График измерений",
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnShowTemperatureHistory)
+ }
+ )
+
+ BleMenuItem(
+ shapeType = ShapeType.End,
+ title = "Изменить пароль",
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnChangePassword)
+ }
+ )
+
+ }
+ )
+
+ Button(
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnShowWriteBlePreview)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ ) {
+ Text(
+ text = "Сохранить"
+ )
+ }
+
+ }
+
+}
+
+@Composable
+fun BleMenuItem(
+ shapeType: ShapeType,
+ title: String,
+ subtitle: String? = null,
+ icon: (@Composable () -> Unit)? = null,
+ onClick: (() -> Unit) = {}
+){
+
+ Surface(
+ onClick = onClick,
+ shape = shapeType.shape,
+ color = MaterialTheme.colorScheme.surfaceContainer
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(
+ vertical = 12.dp,
+ horizontal = 16.dp
+ )
+ ) {
+
+ Column(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ Text(
+ text = title
+ )
+
+ subtitle?.let {
+ Text(
+ color = MaterialTheme.colorScheme.secondary,
+ style = MaterialTheme.typography.bodyMedium,
+ text = it
+ )
+ }
+
+ }
+
+ icon?.invoke()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/LoadingState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/LoadingState.kt
new file mode 100644
index 0000000..ff5febf
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/LoadingState.kt
@@ -0,0 +1,25 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ContainedLoadingIndicator
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun LoadingState(){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ ContainedLoadingIndicator()
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerContract.kt
similarity index 56%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerContract.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerContract.kt
index 364eb0d..ae1dc79 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerContract.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@@ -10,45 +10,39 @@ class ThermometerContract {
sealed class Event : ViewEvent {
- object OnWriteBle : Event()
+ data object OnTxSelect : Event()
- object OnHideWriteBlePreview : Event()
+ data object OnWriteBle : Event()
- object OnShowWriteBlePreview : Event()
+ data object OnHideWriteBlePreview : Event()
- object OnShowTemperatureHistory : Event()
+ data object OnShowWriteBlePreview : Event()
- object OnHideTemperatureHistory : Event()
+ data object OnShowTemperatureHistory : Event()
- object OnChangePassword : Event()
+ data object OnChangePassword : Event()
data class OnSaveHistoryChanged(
val saveHistory: Boolean
) : Event()
- object OnPowerEdit : Event()
-
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
- object OnSaveIntervalEdit : Event()
+ data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
- data class OnBleChanged(
- val ble: Ble.Thermometer
- ) : Event()
-
- object OnNavigateUpClicked : Event()
+ data object OnNavigateUp : Event()
}
sealed class State : ViewState {
- object Loading : State()
+ data object Loading : State()
data class Display(
val origin: Ble.Thermometer,
@@ -66,9 +60,9 @@ class ThermometerContract {
val writeRequest: Ble.Thermometer.WriteRequest
) : WriteState()
- object Success : WriteState()
+ data object Success : WriteState()
- object Failure : WriteState()
+ data object Failure : WriteState()
}
@@ -78,27 +72,29 @@ class ThermometerContract {
sealed class Effect : ViewSideEffect {
- object ShowTemperatureHistory : Effect()
-
- object HideTemperatureHistory : Effect()
-
- object ShowIntervalPicker : Effect()
-
- object HideIntervalPicker : Effect()
-
- object ShowPowerPicker : Effect()
-
- object HidePowerPicker : Effect()
-
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
sealed class Navigation : Effect() {
- object NavigateUp : Navigation()
+ data object Up : Navigation()
- object NavigateToChangePassword : Navigation()
+ data class DurationSelector(
+ val duration: Int
+ ) : Navigation()
+
+ data class TxSelector(
+ val tx: BleView.BleState.TX?
+ ) : Navigation()
+
+ data class ChangePassword(
+ val bleSerial: String
+ ) : Navigation()
+
+ data class ThermometerHistory(
+ val bleSerial: String
+ ) : Navigation()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerScreen.kt
new file mode 100644
index 0000000..e23ccd9
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerScreen.kt
@@ -0,0 +1,182 @@
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
+import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.result.ResultRecipient
+import com.ramcosta.composedestinations.result.onResult
+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.rememberBottomDialogState
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
+import llc.arma.ble.app.ui.screen.locale.localized
+import llc.arma.ble.domain.model.BleInfo
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+enum class SheetPage {
+ WRITE
+}
+
+val Boolean.localizedName: String
+ get() {
+ return if(this){
+ "Включено"
+ } else {
+ "Выключено"
+ }
+ }
+
+@Destination
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ThermometerScreen(
+ bleSerial: String,
+ txSelectResult: ResultRecipient,
+ durationSelectResult: ResultRecipient,
+ navigator: DestinationsNavigator
+) {
+
+ var sheetPage by rememberSaveable {
+ mutableStateOf(null)
+ }
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ val bottomDialog = rememberBottomDialogState()
+
+ txSelectResult.onResult {
+ viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
+ }
+
+ durationSelectResult.onResult {
+ viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
+ }
+
+ LaunchedEffect(sheetPage){
+ when(sheetPage){
+ SheetPage.WRITE -> bottomDialog.show {
+
+ val currentState = viewModel.viewState.value
+
+ if (currentState is ThermometerContract.State.Display) {
+
+ currentState.writeState?.let {
+
+ Write(
+ state = it,
+ onEvent = viewModel::setEvent
+ )
+
+ }
+
+ }
+ }
+ else -> {
+ bottomDialog.hide()
+ }
+ }
+ }
+
+ LaunchedEffect(Unit){
+ viewModel.effect.onEach {
+ when(it){
+ is ThermometerContract.Effect.HideWriteBle -> {
+ sheetPage = null
+ delay(100)
+ }
+ is ThermometerContract.Effect.ShowWriteBle -> {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
+
+ is ThermometerContract.Effect.Navigation.ChangePassword ->
+ navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
+
+ is ThermometerContract.Effect.Navigation.ThermometerHistory ->
+ navigator.navigate(ThermometerHistoryScreenDestination(it.bleSerial))
+
+ is ThermometerContract.Effect.Navigation.TxSelector ->
+ navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
+
+ is ThermometerContract.Effect.Navigation.DurationSelector ->
+ navigator.navigate(DurationSelectorScreenDestination(
+ duration = it.duration,
+ minimum = 60 * 1000,
+ maximum = 240 * 60 * 60 * 1000,
+ minutesComponent = true,
+ secondsComponent = false
+ ))
+
+ ThermometerContract.Effect.Navigation.Up ->
+ navigator.popBackStack()
+
+
+ }
+ }.launchIn(this)
+
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(
+ onClick = {
+ viewModel.setEvent(ThermometerContract.Event.OnNavigateUp)
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text(text = BleInfo.Type.THERMOMETER.localized)
+ }
+ )
+ }
+ ) {
+
+ Column(
+ modifier = Modifier.padding(it)
+ ) {
+
+ when(state){
+ is ThermometerContract.State.Display -> DisplayState(viewModel, state)
+ is ThermometerContract.State.Loading -> LoadingState()
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerViewModel.kt
similarity index 75%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerViewModel.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerViewModel.kt
index 875963d..5c5605c 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/ThermometerViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/ThermometerViewModel.kt
@@ -1,51 +1,108 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
+import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
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.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class ThermometerViewModel @Inject constructor(
+ private val getBleBySerial: GetBleBySerial,
+ private val savedStateHandle: SavedStateHandle,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel() {
+ init {
+
+ val params = ThermometerScreenDestination.argsFrom(savedStateHandle)
+
+ viewModelScope.launch {
+
+ val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
+ onSuccess = { it },
+ onFailure = { null }
+ )
+
+ if(ble != null && ble is Ble.Thermometer){
+ setState {
+
+ when(this){
+ is ThermometerContract.State.Display -> {
+ copy(
+ origin = Ble.Thermometer(
+ info = ble.info,
+ state = origin.state,
+ thermometerState = origin.thermometerState
+ )
+ )
+ }
+ ThermometerContract.State.Loading -> {
+ ThermometerContract.State.Display(
+ origin = ble,
+ thermometer = bleMapper.map(ble) as BleView.Thermometer,
+ writeState = null
+ )
+ }
+ }
+ }
+ }
+
+ }
+
+ }
+
override fun setInitialState() = ThermometerContract.State.Loading
override fun handleEvents(event: ThermometerContract.Event) {
when(event){
- is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
- is ThermometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
+ is ThermometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is ThermometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
- is ThermometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
- is ThermometerContract.Event.OnHideTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
+ is ThermometerContract.Event.OnTxSelect -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ThermometerContract.State,
- event: ThermometerContract.Event.OnNavigateUpClicked
+ event: ThermometerContract.Event.OnTxSelect
) {
- setEffect { ThermometerContract.Effect.Navigation.NavigateUp }
+
+ if(state is ThermometerContract.State.Display){
+
+ setEffect { ThermometerContract.Effect.Navigation.TxSelector(state.thermometer.state.tx) }
+
+ }
+
}
private fun reduce(
+ state: ThermometerContract.State,
+ event: ThermometerContract.Event.OnNavigateUp
+ ) {
+ setEffect { ThermometerContract.Effect.Navigation.Up }
+ }
+
+ /*private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnBleChanged
) {
@@ -69,14 +126,16 @@ class ThermometerViewModel @Inject constructor(
}
}
- }
+ }*/
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit
) {
- setEffect {
- ThermometerContract.Effect.ShowIntervalPicker
+ if(state is ThermometerContract.State.Display) {
+ setEffect {
+ ThermometerContract.Effect.Navigation.DurationSelector(state.thermometer.thermometerState.historyInterval.toInt())
+ }
}
}
@@ -84,41 +143,26 @@ class ThermometerViewModel @Inject constructor(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalChanged
) {
+
if(state is ThermometerContract.State.Display) {
state.thermometer.thermometerState.historyInterval = event.interval
}
- setEffect {
- ThermometerContract.Effect.HideIntervalPicker
- }
-
- }
-
- private fun reduce(
- state: ThermometerContract.State,
- event: ThermometerContract.Event.OnPowerEdit
- ) {
- setEffect {
- ThermometerContract.Effect.ShowPowerPicker
- }
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnPowerChanged
) {
+
if(state is ThermometerContract.State.Display) {
state.thermometer.state.tx = event.tx
}
- setEffect {
- ThermometerContract.Effect.HidePowerPicker
- }
-
}
private fun reduce(
@@ -138,19 +182,14 @@ class ThermometerViewModel @Inject constructor(
event: ThermometerContract.Event.OnShowTemperatureHistory
) {
- setEffect {
- ThermometerContract.Effect.ShowTemperatureHistory
- }
+ if(state is ThermometerContract.State.Display) {
- }
+ setEffect {
+ ThermometerContract.Effect.Navigation.ThermometerHistory(
+ state.origin.info.serial
+ )
+ }
- private fun reduce(
- state: ThermometerContract.State,
- event: ThermometerContract.Event.OnHideTemperatureHistory
- ) {
-
- setEffect {
- ThermometerContract.Effect.HideTemperatureHistory
}
}
@@ -286,7 +325,7 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display){
setEffect {
- ThermometerContract.Effect.Navigation.NavigateToChangePassword
+ ThermometerContract.Effect.Navigation.ChangePassword(state.thermometer.info.serial)
}
}
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/main/Write.kt
similarity index 96%
rename from app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/Write.kt
rename to app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/main/Write.kt
index 0677256..e470714 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/main/Write.kt
@@ -1,4 +1,4 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
+package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@@ -21,9 +21,7 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
-import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
-import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
+import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
@@ -52,6 +50,7 @@ fun Write(
state.writeRequest.tx?.let {
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db"
)
@@ -61,6 +60,7 @@ fun Write(
state.writeRequest.saveHistory?.let {
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = it.localizedName
)
@@ -73,6 +73,7 @@ fun Write(
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
BleMenuItem(
+ shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин."
)
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/DisplayState.kt
deleted file mode 100644
index 03e6cef..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/DisplayState.kt
+++ /dev/null
@@ -1,313 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
-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.material.icons.rounded.KeyboardArrowRight
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Switch
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.BleInfoView
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
-import llc.arma.ble.domain.model.Ble
-
-@Composable
-fun DisplayState(
- onEvent: (ThermometerContract.Event) -> Unit,
- origin: Ble.Thermometer,
- ble: BleView.Thermometer
-) {
-
- val scrollState = rememberScrollState()
-
- Column {
-
- Column(
- modifier = Modifier
- .verticalScroll(scrollState)
- .weight(1f)
- ) {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
- BleInfoView(
- bleInfo = origin.info,
- version = origin.state.version
- )
- }
-
- Column(
- modifier = Modifier,
- content = {
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .clickable {
- onEvent(ThermometerContract.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 = "Температура"
- )
-
- Text(
- color = MaterialTheme.colorScheme.secondary,
- style = MaterialTheme.typography.bodyMedium,
- text = "${ble.thermometerState.temperature.value} °C"
- )
-
- }
-
- }
-
- }
-
- Box(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 8.dp
- )
- ) {
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clip(RoundedCornerShape(16.dp))
- .clickable { }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Сохранять историю измерений"
- )
-
- }
-
- Switch(
- checked = ble.thermometerState.saveHistory,
- onCheckedChange = {
- onEvent(ThermometerContract.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(ThermometerContract.Event.OnSaveIntervalEdit)
- }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Интервал измерений"
- )
-
- val hours = ble.thermometerState.historyInterval / 1000 / 60 / 60
- val minutes = (ble.thermometerState.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(ThermometerContract.Event.OnShowTemperatureHistory)
- }
- .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(ThermometerContract.Event.OnChangePassword)
- }
- .padding(8.dp)
- ) {
-
- Column(
- modifier = Modifier.weight(1f)
- ) {
-
- Text(
- text = "Изменить пароль"
- )
-
- }
-
- Icon(
- imageVector = Icons.Rounded.KeyboardArrowRight,
- contentDescription = null
- )
-
- }
-
- }
-
- }
- )
-
- }
-
- PrimaryButton(
- modifier = Modifier.shadow(
- if(scrollState.canScrollForward){
- 8.dp
- } else {
- 0.dp
- }
- ).background(MaterialTheme.colorScheme.background),
- label = "Сохранить"
- ) {
- onEvent(ThermometerContract.Event.OnShowWriteBlePreview)
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/IntervalEdit.kt
deleted file mode 100644
index 600d285..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/IntervalEdit.kt
+++ /dev/null
@@ -1,200 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
-
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.KeyboardArrowDown
-import androidx.compose.material.icons.rounded.KeyboardArrowUp
-import androidx.compose.material3.FilledIconButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
-
-@Composable
-fun IntervalEdit(
- state: BleView.Thermometer,
- onEvent: (ThermometerContract.Event) -> Unit,
-){
-
- var value by remember(state.thermometerState.historyInterval) {
- mutableIntStateOf((state.thermometerState.historyInterval / 1000 / 60 / 60).toInt())
- }
-
- val maxInterval = 240
-
- if(value > maxInterval){
- value = maxInterval
- }
-
- if(value < 1){
- value = 1
- }
-
- val maxHours = maxInterval
- val maxDays = maxInterval / 24
-
- val dayValue = value / 24
- val hourValue = value - (24 * dayValue)
-
- Column(
- modifier = Modifier
- ) {
-
- Text(
- modifier = Modifier.padding(horizontal = 12.dp),
- text = "Интервал измерений",
- style = MaterialTheme.typography.titleLarge
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.align(Alignment.CenterHorizontally)
- ) {
-
- NumberPicker(
- range = -1..maxDays,
- value = dayValue,
- onValueChanged = { value = (it * 24) + hourValue }
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Text(text = "Дни")
-
- Spacer(modifier = Modifier.width(16.dp))
-
- NumberPicker(
- range = -1..maxHours,
- value = hourValue,
- onValueChanged = {
- value = it + (dayValue * 24)
- }
- )
-
- Spacer(modifier = Modifier.width(8.dp))
-
- Text(text = "Часы")
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(
- ThermometerContract.Event.OnSaveIntervalChanged(
- value.toLong() * 1000 * 60 * 60
- )
- )
- }
-
- }
-
-}
-
-@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/thermometer/view/LoadingState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/LoadingState.kt
deleted file mode 100644
index c8d83cb..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/LoadingState.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-
-@Composable
-fun LoadingState(){
-
- Box(modifier = Modifier.fillMaxSize()) {
-
- CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
-
- }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index 2407f5e..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/PowerEdit.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import llc.arma.ble.app.ui.common.PrimaryButton
-import llc.arma.ble.app.ui.model.BleView
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.SelectorItem
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
-
-@Composable
-fun PowerEdit(
- state: BleView.Thermometer,
- onEvent: (ThermometerContract.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.entries.forEach {
-
- SelectorItem(
- label = "${it.value} dBb (${it.powerPercentage} %)",
- selected = it == value
- ){
- value = it
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- PrimaryButton(
- label = "Применить"
- ) {
- onEvent(
- ThermometerContract.Event.OnPowerChanged(
- value
- )
- )
- }
-
- }
-
-}
\ No newline at end of file
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
deleted file mode 100644
index b8ed1be..0000000
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/thermometer/view/TemperatureHistory.kt
+++ /dev/null
@@ -1,373 +0,0 @@
-package llc.arma.ble.app.ui.screen.inspection.thermometer.view
-
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.scrollBy
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Refresh
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.StrokeCap
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.viewModelScope
-import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
-import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
-import com.patrykandpatrick.vico.compose.chart.Chart
-import com.patrykandpatrick.vico.compose.chart.line.lineChart
-import com.patrykandpatrick.vico.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.ChartEntryModelProducer
-import dagger.hilt.android.lifecycle.HiltViewModel
-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.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.common.ProgressState
-import llc.arma.ble.domain.model.Ble
-import llc.arma.ble.domain.model.BleInfo
-import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
-import javax.inject.Inject
-
-class TemperatureEntry(
- val localDate: Long,
- override val x: Float,
- override val y: Float,
-) : ChartEntry {
-
- override fun withY(y: Float) = TemperatureEntry(localDate, x, y)
-
-}
-
-val formatter = SimpleDateFormat("dd.MM HH:mm", Locale.getDefault())
-
-@Composable
-fun TemperatureHistory(
- ble: BleInfo
-) {
-
- val viewModel = hiltViewModel()
- val state = viewModel.viewState.value
-
- LaunchedEffect(ble.serial) {
- viewModel.setEvent(TemperatureHistoryContract.Event.OnStart(ble.serial))
- }
-
- Column(
- modifier = Modifier.fillMaxHeight(0.9f)
- ) {
-
- Row(
- modifier = Modifier.padding(horizontal = 12.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
-
- val title = when(state){
- is TemperatureHistoryContract.State.Display -> {
- when (state.loadingHistoryState) {
- is ProgressState.Finished -> "История измерений (${state.loadingHistoryState.data.size})"
- is ProgressState.Indeterminate -> "История измерений"
- is ProgressState.Progress -> "История измерений"
- }
- }
- TemperatureHistoryContract.State.Exception -> "История измерений"
- }
-
- Text(
- modifier = Modifier.weight(1f),
- text = title,
- style = MaterialTheme.typography.titleLarge
- )
-
- IconButton(
- onClick = {
- viewModel.setEvent(TemperatureHistoryContract.Event.OnRefreshHistory(ble.serial))
- },
- enabled = when(state){
- is TemperatureHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
- TemperatureHistoryContract.State.Exception -> true
- }
- ) {
- Icon(
- imageVector = Icons.Rounded.Refresh,
- contentDescription = null
- )
- }
-
- }
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Box(modifier = Modifier) {
-
- when (state) {
- is TemperatureHistoryContract.State.Display -> Display(state = state)
- TemperatureHistoryContract.State.Exception -> Exception()
- }
-
- }
-
- }
-
-}
-
-
-@Composable
-fun Display(
- state: TemperatureHistoryContract.State.Display
-) {
-
- Box(modifier = Modifier
- .padding(8.dp)
- .fillMaxSize()
- ) {
-
- when (state.loadingHistoryState) {
-
- is ProgressState.Finished -> {
-
- if(state.loadingHistoryState.data.isEmpty()){
-
- Text(
- modifier = Modifier.align(Alignment.Center),
- text = "Нет данных"
- )
-
- } else {
-
- val producer = remember(state.loadingHistoryState.data) {
- state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
- TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
- }.let {
- ChartEntryModelProducer(it)
- }
- }
-
- val axisValueFormatter =
- AxisValueFormatter { value, chartValues ->
- (chartValues.chartEntryModel.entries.firstOrNull()
- ?.getOrNull(value.toInt()) as? TemperatureEntry)
- ?.localDate
- ?.let { formatter.format(Date(it)) }
- .orEmpty()
- }
-
- val lineChart = lineChart()
-
- val scrollState = rememberChartScrollState()
-
- Chart(
- chartScrollState = scrollState,
- chart = lineChart,
- chartModelProducer = producer,
- startAxis = startAxis(),
- bottomAxis = bottomAxis(
- tickLength = 0.dp,
- valueFormatter = axisValueFormatter,
- labelRotationDegrees = -90f,
- ),
- modifier = Modifier.fillMaxSize(),
- )
-
- LaunchedEffect(scrollState.maxValue) {
- scrollState.scrollBy(scrollState.maxValue)
- }
-
- }
-
- }
- is ProgressState.Indeterminate -> {
-
- CircularProgressIndicator(
- strokeCap = StrokeCap.Round,
- modifier = Modifier.align(Alignment.Center)
- )
-
- }
- is ProgressState.Progress -> {
-
- val progressAnimDuration = 1500
- val progressAnimation by animateFloatAsState(
- targetValue = state.loadingHistoryState.value,
- animationSpec = tween(
- durationMillis = progressAnimDuration,
- easing = FastOutSlowInEasing
- ), 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 TemperatureHistoryContract {
-
- sealed class Event : ViewEvent {
-
- data class OnStart(
- val serial: String
- ) : Event()
-
- data class OnRefreshHistory(
- val serial: String
- ) : Event()
-
- }
-
- sealed class State : ViewState {
-
- data class Display(
- val loadingHistoryState : ProgressState>
- ) : State()
-
- object Exception : State()
-
- }
-
- sealed class Effect : ViewSideEffect {
-
- }
-
-}
-
-
-
-@HiltViewModel
-class TemperatureHistoryViewModel @Inject constructor(
- private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
-) : BaseViewModel() {
-
- private var lastSerial: String? = null
-
- override fun setInitialState() = TemperatureHistoryContract.State.Display(
- ProgressState.Indeterminate
- )
-
- override fun handleEvents(event: TemperatureHistoryContract.Event) {
- when(event){
- is TemperatureHistoryContract.Event.OnStart -> reduce(viewState.value, event)
- is TemperatureHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
- }
- }
-
- private fun reduce(
- state: TemperatureHistoryContract.State,
- event: TemperatureHistoryContract.Event.OnStart
- ) {
- viewModelScope.launch {
-
- if(state is TemperatureHistoryContract.State.Display) {
-
- if(lastSerial != event.serial) {
-
- lastSerial = event.serial
-
- setState {
- TemperatureHistoryContract.State.Display(ProgressState.Indeterminate)
- }
-
- getTemperatureHistoryBySerial(event.serial).onEach {
- it.fold(
- onSuccess = {
- setState {
- TemperatureHistoryContract.State.Display(it)
- }
- },
- onFailure = {
- setState {
- TemperatureHistoryContract.State.Exception
- }
- }
- )
- }.launchIn(this)
-
- }
-
- }
-
- }
-
- }
-
- private fun reduce(
- state: TemperatureHistoryContract.State,
- event: TemperatureHistoryContract.Event.OnRefreshHistory
- ) {
- viewModelScope.launch {
-
- setState {
- TemperatureHistoryContract.State.Display(ProgressState.Indeterminate)
- }
-
- getTemperatureHistoryBySerial(event.serial).onEach {
- it.fold(
- onSuccess = {
- setState {
- TemperatureHistoryContract.State.Display(it)
- }
- },
- onFailure = {
- setState {
- TemperatureHistoryContract.State.Exception
- }
- }
- )
- }.launchIn(this)
-
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/locale/Locale.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/locale/Locale.kt
index 0e064fd..c10199b 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/locale/Locale.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/locale/Locale.kt
@@ -7,7 +7,7 @@ import androidx.compose.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.ui.graphics.vector.ImageVector
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.RealtimeViewMode
+import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
@@ -61,7 +61,7 @@ val BleFilter.Field.localized: String
val BleInfo.Type?.localized: String
get() {
return when(this){
- BleInfo.Type.HOST -> "Хост"
+ BleInfo.Type.HOST -> "Шлюз"
BleInfo.Type.BEACON -> "Маяк"
BleInfo.Type.THERMOMETER -> "Термодатчик"
BleInfo.Type.ACCELEROMETER -> "Акселерометр"
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
index 2156074..51874b5 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt
@@ -1,50 +1,310 @@
package llc.arma.ble.app.ui.screen.main
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Text
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.navigation.ModalBottomSheetLayout
+import androidx.compose.material.navigation.rememberBottomSheetNavigator
import androidx.compose.runtime.Composable
-import androidx.compose.ui.window.DialogProperties
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.dialog
+import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
-import llc.arma.ble.app.ui.screen.ble.BleListContract
-import llc.arma.ble.app.ui.screen.ble.BleListScreen
-import llc.arma.ble.app.ui.screen.connection.ConnectionContract
-import llc.arma.ble.app.ui.screen.connection.ConnectionScreen
-import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
-import llc.arma.ble.app.ui.screen.password.ChangePasswordScreen
-import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteContract
-import llc.arma.ble.app.ui.screen.rotation.delete.RotationDeleteScreen
-import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticContract
-import llc.arma.ble.app.ui.screen.rotation.statistic.RotationStatisticScreen
+import com.ramcosta.composedestinations.DestinationsNavHost
+import com.ramcosta.composedestinations.generated.NavGraphs
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen() {
- val controller = rememberNavController()
+ val navController = rememberNavController()
+
+ val bottomSheetNavigator = rememberBottomSheetNavigator()
+ navController.navigatorProvider.addNavigator(bottomSheetNavigator)
+
+ ModalBottomSheetLayout(
+ bottomSheetNavigator = bottomSheetNavigator,
+ sheetShape = RoundedCornerShape(16.dp, 16.dp, 0.dp, 0.dp)
+ ) {
+ DestinationsNavHost(
+ navController = navController,
+ navGraph = NavGraphs.root
+ )
+ }
+
+ //DestinationsNavHost(navGraph = NavGraphs.root)
+
+ /*val controller = rememberNavController()
NavHost(
navController = controller,
- startDestination = "bleList",
+ startDestination = BleListRoute,
builder = {
- composable(
- route = "bleList",
- content = {
+ dialog {
- BleListScreen(
+ ChangePasswordScreen(
+ onNavigationEvent = {
+ when(it){
+ ChangePasswordContract.Effect.Navigation.Up ->
+ controller.popBackStack()
+ }
+ }
+ )
+
+ }
+
+ dialog {
+
+ val params = it.savedStateHandle.toRoute()
+
+ println(params.requestId)
+
+ ModalBottomSheet(
+ onDismissRequest = controller::popBackStack
+ ) {
+
+ DurationSelectorScreen(
onNavigationEvent = {
when(it){
- is BleListContract.Effect.Navigation.NavigateToBle -> controller.navigate("connection/${it.serial}")
+ DurationSelectorContract.Effect.Navigation.Up ->
+ controller.popBackStack()
+
+ is DurationSelectorContract.Effect.Navigation.UpWithResult -> {
+ controller.previousBackStackEntry?.savedStateHandle?.set(
+ params.requestId,
+ it.duration
+ )
+ controller.popBackStack()
+ }
}
}
)
}
- )
+
+ }
+
+ composable {
+
+ val txSelectResult = controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow("txSelectResult", null)
+ ?.collectAsState()?.value
+
+ BeaconScreen(
+ txSelectResult = txSelectResult,
+ onNavigationEvent = {
+ when(it){
+ is BeaconContract.Effect.Navigation.PasswordForm ->
+ controller.navigate(PasswordFormRoute(it.bleSerial))
+
+ BeaconContract.Effect.Navigation.Up ->
+ controller.popBackStack()
+
+ is BeaconContract.Effect.Navigation.TxSelector ->
+ controller.navigate(TxPowerSelectorRoute(it.tx))
+ }
+ }
+ )
+ }
+
+ composable {
+
+ val txSelectResult = controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow("txSelectResult", null)
+ ?.collectAsState()?.value
+
+ AccelerometerScreen (
+ onNavigationEvent = {
+ when(it){
+ is AccelerometerContract.Effect.Navigation.AccelHistory -> TODO()
+ is AccelerometerContract.Effect.Navigation.AccelRealtime -> TODO()
+ is AccelerometerContract.Effect.Navigation.AccelSpectre -> TODO()
+ is AccelerometerContract.Effect.Navigation.ChangePassword ->
+ controller.navigate(PasswordFormRoute(it.serial))
+
+ is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
+ controller.navigate(TxPowerSelectorRoute(it.tx))
+ }
+ }
+ )
+ }
+
+ composable {
+ GateHistoryScreen(
+ onNavigationEvent = {
+ when(it){
+ GateHistoryContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ }
+ }
+ )
+ }
+
+ composable {
+
+ GateBleTableScreen(
+ onEvent = {
+ when(it){
+ GateBleTableContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ val savedState = remember {
+ controller.currentBackStackEntry
+ ?.savedStateHandle
+ }
+
+ val txSelectResult = savedState
+ ?.getStateFlow("txSelectResult", null)
+ ?.collectAsState()?.value
+
+ val readDurationSelectResult = savedState
+ ?.getStateFlow("GateReadDuration", null)
+ ?.collectAsState()?.value
+
+ GateScreen(
+ readDurationSelectResult = readDurationSelectResult,
+ txSelectResult = txSelectResult,
+ onNavigationEvent = {
+ when(it){
+ is GateContract.Effect.Navigation.BleTable ->
+ controller.navigate(GateBleTableRoute(it.serial))
+
+ is GateContract.Effect.Navigation.ChangePassword ->
+ controller.navigate(PasswordFormRoute(it.serial))
+
+ is GateContract.Effect.Navigation.GateHistory ->
+ controller.navigate(GateHistoryRoute(it.ble.serial))
+
+ is GateContract.Effect.Navigation.Up ->
+ controller.popBackStack()
+
+ is GateContract.Effect.Navigation.TxSelector ->
+ controller.navigate(TxPowerSelectorRoute(it.tx))
+
+ is GateContract.Effect.Navigation.ReadIntervalSelector ->
+ controller.navigate(DurationSelectorRoute(
+ "GateReadDuration",
+ it.interval
+ ))
+ }
+ }
+ )
+
+ controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.remove("txSelectResult")
+
+ controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.remove("GateReadDuration")
+
+ }
+
+ composable {
+
+ val txSelectResult = controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow("txSelectResult", null)
+ ?.collectAsState()?.value
+
+ ThermometerScreen(
+ txSelectResult = txSelectResult,
+ onNavigationEvent = {
+ when(it){
+ ThermometerContract.Effect.Navigation.ChangePassword -> TODO()
+
+ is ThermometerContract.Effect.Navigation.ThermometerHistory ->
+ controller.navigate(ThermometerHistoryRoute(it.bleSerial))
+
+ ThermometerContract.Effect.Navigation.Up ->
+ controller.popBackStack()
+
+ is ThermometerContract.Effect.Navigation.TxSelector ->
+ controller.navigate(TxPowerSelectorRoute(it.tx))
+ }
+ }
+ )
+
+ controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.remove("txSelectResult")
+
+ }
+
+ composable {
+
+ TxPowerSelectorScreen(
+ onNavigationEvent = {
+ when(it){
+ is TxPowerSelectorContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ is TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult -> {
+
+ controller.previousBackStackEntry?.savedStateHandle?.set(
+ "txSelectResult",
+ it.tx
+ )
+
+ controller.popBackStack()
+ }
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ BleFilterScreen {
+ when(it){
+ BleFilterContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ }
+ }
+
+ }
+
+ composable {
+ ThermometerHistoryScreen(
+ onNavigationEvent = {
+ when(it){
+ ThermometerHistoryContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ }
+ }
+ )
+ }
+
+ composable {
+
+ BleListScreen(
+ onNavigationEvent = {
+ when (it) {
+ is BleListContract.Effect.Navigation.BleFilter ->
+ controller.navigate(BleFilterRoute)
+
+ is BleListContract.Effect.Navigation.Thermometer ->
+ controller.navigate(ThermometerRoute(it.serial))
+
+ is BleListContract.Effect.Navigation.Gate ->
+ controller.navigate(GateRoute(it.serial))
+
+ is BleListContract.Effect.Navigation.Beacon ->
+ controller.navigate(BeaconRoute(it.serial))
+
+ is BleListContract.Effect.Navigation.Accelerometer ->
+ controller.navigate(AccelerometerRoute(it.serial))
+ }
+ }
+ )
+
+ }
+
composable(
route = "connection/{serial}",
@@ -58,6 +318,9 @@ fun MainScreen() {
controller.navigate("change_password/${it.serial}")
is ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic ->
controller.navigate("rotation_statistic/${it.serial}")
+
+ is ConnectionContract.Effect.Navigation.NavigateToThermometerHistory ->
+ controller.navigate(ThermometerHistoryRoute(it.bleSerial))
}
}
)
@@ -101,7 +364,7 @@ fun MainScreen() {
content = {
ChangePasswordScreen {
when(it){
- is ChangePasswordContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
+ is ChangePasswordContract.Effect.Navigation.Up -> controller.navigateUp()
}
}
}
@@ -109,6 +372,6 @@ fun MainScreen() {
}
- )
+ )*/
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt
index 01f94a2..4c8493e 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordContract.kt
@@ -53,7 +53,7 @@ class ChangePasswordContract {
sealed class Navigation : Effect() {
- object NavigateUp : Navigation()
+ object Up : Navigation()
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt
index 4c15535..2b19ba4 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordScreen.kt
@@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen.password
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -12,9 +13,11 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
+import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
@@ -24,6 +27,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -32,6 +36,11 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootGraph
+import com.ramcosta.composedestinations.generated.navgraphs.RootNavGraph
+import com.ramcosta.composedestinations.navigation.DestinationsNavigator
+import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.PrimaryButton
@@ -39,9 +48,11 @@ import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.password.view.Loading
import llc.arma.ble.app.ui.screen.password.view.Result
+@Destination(style = DestinationStyle.Dialog::class)
@Composable
fun ChangePasswordScreen(
- onNavigationEvent: (ChangePasswordContract.Effect.Navigation) -> Unit
+ bleSerial: String,
+ navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel()
@@ -50,30 +61,30 @@ fun ChangePasswordScreen(
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
- is ChangePasswordContract.Effect.Navigation -> onNavigationEvent(it)
+ ChangePasswordContract.Effect.Navigation.Up -> navigator.popBackStack()
}
}.launchIn(this)
}
Surface(
- shape = RoundedCornerShape(16.dp),
+ shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
- .wrapContentHeight()
- .padding(24.dp),
+ .wrapContentHeight(),
) {
- Column {
-
- Spacer(modifier = Modifier.height(20.dp))
+ Column(
+ modifier = Modifier.padding(20.dp)
+ ) {
Text(
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.titleLarge,
- text = "Изменение пароля",
- textAlign = TextAlign.Center
+ text = "Изменение пароля"
)
+ Spacer(modifier = Modifier.height(20.dp))
+
if(state.loading != null){
when(state.loading){
@@ -95,7 +106,7 @@ fun ChangePasswordScreen(
} else {
Column(
- modifier = Modifier.padding(20.dp)
+ modifier = Modifier
) {
@@ -209,16 +220,31 @@ fun ChangePasswordScreen(
Spacer(modifier = Modifier.height(20.dp))
- PrimaryButton(
- label = "Применить"
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.align(Alignment.End)
) {
- viewModel.setEvent(ChangePasswordContract.Event.OnChange)
- }
- SecondaryButton(
- label = "Отменить"
- ) {
- viewModel.setEvent(ChangePasswordContract.Event.OnNavigateUp)
+ OutlinedButton(
+ onClick = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnNavigateUp)
+ }
+ ) {
+ Text(
+ text = "Отменить"
+ )
+ }
+
+ Button(
+ onClick = {
+ viewModel.setEvent(ChangePasswordContract.Event.OnChange)
+ }
+ ) {
+ Text(
+ text = "Применить"
+ )
+ }
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt
index 1dd9b55..b89af79 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/password/ChangePasswordViewModel.kt
@@ -117,7 +117,7 @@ class ChangePasswordViewModel @Inject constructor(
) {
setEffect {
- ChangePasswordContract.Effect.Navigation.NavigateUp
+ ChangePasswordContract.Effect.Navigation.Up
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/theme/Theme.kt b/app/src/main/java/llc/arma/ble/app/ui/theme/Theme.kt
index fb197e4..9d626a1 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/theme/Theme.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/theme/Theme.kt
@@ -1,19 +1,22 @@
package llc.arma.ble.app.ui.theme
import android.app.Activity
+import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
-import llc.arma.nfc.app.ui.theme.Typography
-import llc.arma.nfc.app.ui.theme.shapes
-val LightColorScheme = lightColorScheme(
+/*val LightColorScheme = lightColorScheme(
primary = Color(0xFF1B1B1F),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFF2D2C27),
@@ -69,4 +72,40 @@ fun BleTheme(
content = content,
shapes = shapes
)
+}*/
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+)
+
+@Composable
+fun BleTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/theme/Type.kt b/app/src/main/java/llc/arma/ble/app/ui/theme/Type.kt
index a34089d..bc9a5a9 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/theme/Type.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/theme/Type.kt
@@ -1,4 +1,4 @@
-package llc.arma.nfc.app.ui.theme
+package llc.arma.ble.app.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
@@ -26,7 +26,7 @@ val shapes = Shapes(
)
val Typography = Typography(
- headlineLarge = TextStyle(
+ /*headlineLarge = TextStyle(
fontFamily = font,
fontWeight = FontWeight.W400,
fontSize = 32.sp,
@@ -109,5 +109,5 @@ val Typography = Typography(
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.sp
- )
+ )*/
)
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
index 9ed1d2f..4ba5a1e 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
@@ -2,7 +2,6 @@ package llc.arma.ble.data.repository
import android.Manifest
import android.app.Application
-import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresPermission
import kotlinx.coroutines.CoroutineScope
@@ -12,10 +11,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.fromByte
@@ -38,12 +38,16 @@ import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
+import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectOptions
import no.nordicsemi.android.kotlin.ble.core.scanner.BleNumOfMatches
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerCallbackType
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerMatchMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
+import no.nordicsemi.kotlin.ble.client.android.CentralManager
+import no.nordicsemi.kotlin.ble.client.android.Peripheral
+import no.nordicsemi.kotlin.ble.client.android.native
import java.util.Collections
import java.util.Timer
import java.util.TimerTask
@@ -52,6 +56,7 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.PI
import kotlin.math.atan
+import kotlin.uuid.ExperimentalUuidApi
val versionServiceUUID: UUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb")
@@ -77,57 +82,77 @@ class BleRepositoryImpl @Inject constructor(
private val rotationsDao: RotationsDao
) : BleRepository {
- val resultList: MutableMap = Collections.synchronizedMap(mutableMapOf())
+ private val resultList: MutableMap = Collections.synchronizedMap(mutableMapOf())
override fun getFoundBle(): List {
return resultList.values.toList()
}
- override fun getBleAroundFlow(): Result>, BleException> {
+ @RequiresPermission(allOf = [
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_CONNECT
+ ])
+ override fun getBleAroundFlow() = callbackFlow {
- return if(app.checkPermission()){
+ val job = BleScanner(app)
+ .scan(
+ settings = BleScannerSettings(
+ includeStoredBondedDevices = false,
+ scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
+ callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
+ matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
+ numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
+ reportDelay = 0L
+ )
+ ).filter { it.device.name?.contains("ArmA") == true }
+ .onEach {
+ resultList[it.device.address] = it.info
+ }.launchIn(CoroutineScope(Dispatchers.IO))
- Result.success(
+ val timer = Timer().apply {
+ schedule(object : TimerTask() {
+ override fun run() {
+ CoroutineScope(Dispatchers.IO).launch {
+ send(resultList.values.toList())
+ }
+ }
+ }, 500, 500)
+ }
- callbackFlow {
+ awaitClose {
+ job.cancel()
+ timer.cancel()
+ }
- val job = BleScanner(app)
- .scan(
- settings = BleScannerSettings(
- includeStoredBondedDevices = false,
- scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
- callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
- matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
- numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
- reportDelay = 0L
- )
- ).filter { it.device.name?.contains("ArmA") == true }
- .onEach {
- resultList[it.device.address] = it.info
- }.launchIn(CoroutineScope(Dispatchers.IO))
+ }
+
+ override suspend fun getBleInfoFlow(
+ serial: String
+ ): Flow {
+
+ return callbackFlow {
+
+ var working = true
+
+ launch {
+
+ while (working) {
+
+ resultList[serial]?.let { ble ->
+
+ send(ble)
- val timer = Timer().apply {
- schedule(object : TimerTask() {
- override fun run() {
- CoroutineScope(Dispatchers.IO).launch {
- send(resultList.values.toList())
- }
- }
- }, 500, 500)
}
- awaitClose {
- job.cancel()
- timer.cancel()
- }
+ delay(5_000)
}
- )
+ }
- } else {
-
- Result.failure(BleException.PermissionDenied)
+ awaitClose {
+ working = false
+ }
}
@@ -135,8 +160,9 @@ class BleRepositoryImpl @Inject constructor(
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override suspend fun getBleBySerial(
- serial: String
- ): Result, BleException> {
+ serial: String,
+ scope: CoroutineScope
+ ): Result {
val initialBle = resultList[serial]
@@ -146,26 +172,39 @@ class BleRepositoryImpl @Inject constructor(
} else {
- val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.IO))
+ println("Start")
+
+ val centralManager = CentralManager.Factory.native(app, scope)
+ val peripheral = centralManager.getPeripheralById(serial)
+
+ if(peripheral!= null) {
+
+ withTimeout(10000) {
+ centralManager.connect(
+ peripheral,
+ options = CentralManager.ConnectionOptions.AutoConnect,
+ )
+ }
+
+ val version = peripheral.readVersion()
+
+ println("connected ${version}")
+
+ }
+
+ println("End")
+
+ var connection: ClientBleGatt? = null
try {
- fun BleInfo.updateBleInfo(): BleInfo {
- return copy(
- rssi = if ((SystemClock.elapsedRealtime() - scanTime) > 15_000) {
- null
- } else {
- rssi
- }
- )
- }
+ connection = ClientBleGatt.connect(app, serial, scope, BleGattConnectOptions(false))
val version = connection.readVersion()
fun BleInfo.updateState(): Ble.BleState {
return Ble.BleState(
- tx = Ble.BleState.TX.fromByte(tx.toByte())
- ?: Ble.BleState.TX.ZERO,
+ tx = Ble.BleState.TX.fromByte(tx.toByte()) ?: Ble.BleState.TX.ZERO,
version = version
)
}
@@ -174,123 +213,64 @@ class BleRepositoryImpl @Inject constructor(
when (initialBle.type) {
BleInfo.Type.HOST -> {
- val tState = connection.readHostState().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
-
- Ble.Host(
- info = initialBle.updateBleInfo(),
+ Ble.Gate(
+ info = initialBle,
state = initialBle.updateState(),
- hostState = tState
+ gateState = connection.readHostState().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
)
}
BleInfo.Type.BEACON -> {
Ble.Beacon(
- info = initialBle.updateBleInfo(),
+ info = initialBle,
state = initialBle.updateState(),
)
}
BleInfo.Type.THERMOMETER -> {
- val tState = connection.readThermometerState(
- initialBle.tableStatus
- ).fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
-
Ble.Thermometer(
- info = initialBle.updateBleInfo(),
+ info = initialBle,
state = initialBle.updateState(),
- thermometerState = tState
+ thermometerState = connection.readThermometerState(
+ initialBle.tableStatus
+ ).fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
)
}
BleInfo.Type.ACCELEROMETER -> {
- val tState = connection.readAccelState(
- initialBle.tableStatus,
- version
- ).fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
-
Ble.Accelerometer(
- info = initialBle.updateBleInfo(),
+ info = initialBle,
state = initialBle.updateState(),
- accelerometerState = tState
+ accelerometerState = connection.readAccelState(
+ initialBle.tableStatus,
+ version
+ ).fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
)
}
-
}
- Result.success(
- flow {
- while (true) {
-
- resultList[serial]?.let { ble ->
-
- firstResult.state = ble.updateState()
- firstResult.info = ble.updateBleInfo()
-
- emit(
- when (firstResult) {
- is Ble.Accelerometer -> {
- Ble.Accelerometer(
- info = ble.updateBleInfo(),
- state = ble.updateState(),
- accelerometerState = firstResult.accelerometerState
- )
- }
-
- is Ble.Beacon -> {
- Ble.Beacon(
- info = ble.updateBleInfo(),
- state = ble.updateState()
- )
- }
-
- is Ble.Host -> {
- Ble.Host(
- info = ble.updateBleInfo(),
- state = ble.updateState(),
- hostState = firstResult.hostState
- )
- }
-
- is Ble.Thermometer -> {
- Ble.Thermometer(
- info = ble.updateBleInfo(),
- state = ble.updateState(),
- thermometerState = firstResult.thermometerState
- )
- }
- }
- )
-
- }
-
- delay(1_000)
-
- }
-
- }
-
- )
+ Result.success(firstResult)
} catch (err: Throwable) {
err.printStackTrace()
return Result.failure(BleException.UnexpectedResponse)
} finally {
- connection.close()
+ connection?.close()
}
@@ -319,10 +299,7 @@ class BleRepositoryImpl @Inject constructor(
characteristic = service.findCharacteristic(intervalReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
- val interval = characteristic.readWriteInterval().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
+ val interval = characteristic.readWriteInterval()
return Result.success(
Ble.Thermometer.ThermometerState(
@@ -337,7 +314,7 @@ class BleRepositoryImpl @Inject constructor(
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readHostState(
- ): Result {
+ ): Result {
val service = discoverServices().findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
@@ -345,18 +322,11 @@ class BleRepositoryImpl @Inject constructor(
val characteristic = service.findCharacteristic(intervalReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
- val interval = characteristic.readWriteInterval().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
-
- val readTimeout = characteristic.readTimeoutInterval().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
+ val interval = characteristic.readWriteInterval()
+ val readTimeout = characteristic.readTimeoutInterval()
return Result.success(
- Ble.Host.HostState(
+ Ble.Gate.HostState(
historyInterval = interval,
readInterval = readTimeout
)
@@ -364,54 +334,32 @@ class BleRepositoryImpl @Inject constructor(
}
- private suspend fun ClientBleGattCharacteristic.readWriteInterval(
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ private suspend fun ClientBleGattCharacteristic.readWriteInterval(): Long {
- ): Result {
+ write(DataByteArray.from(3, 0, 0, 0))
- return if(app.checkPermission().not()) {
-
- Result.failure(BleException.PermissionDenied)
-
- } else {
-
- write(DataByteArray.from(3, 0, 0, 0))
-
- val readTimeout = read().value.let {
- if(it.size == 4){
- it.get4byteUIntAt(0).toLong()
- } else {
- 0
- }
+ return read().value.let {
+ if(it.size == 4){
+ it.get4byteUIntAt(0).toLong()
+ } else {
+ 0
}
-
- Result.success(readTimeout)
-
}
}
- private suspend fun ClientBleGattCharacteristic.readTimeoutInterval(
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ private suspend fun ClientBleGattCharacteristic.readTimeoutInterval(): Long {
- ): Result {
+ write(DataByteArray.from(8, 0, 0, 0))
- return if(app.checkPermission().not()) {
-
- Result.failure(BleException.PermissionDenied)
-
- } else {
-
- write(DataByteArray.from(8, 0, 0, 0))
-
- val interval = read().value.let {
- if (it.size == 4) {
- it.get4byteUIntAt(0).toLong()
- } else {
- 0
- }
+ return read().value.let {
+ if (it.size == 4) {
+ it.get4byteUIntAt(0).toLong()
+ } else {
+ 0
}
-
- Result.success(interval)
-
}
}
@@ -472,6 +420,21 @@ class BleRepositoryImpl @Inject constructor(
}
+
+
+ @OptIn(ExperimentalUuidApi::class)
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ private suspend fun Peripheral.readVersion(): Version {
+
+ return services().filter { it.isNotEmpty() }.firstOrNull()
+ ?.firstOrNull { it.uuid.toString() == versionServiceUUID.toString() }
+ ?.characteristics?.firstOrNull { it.uuid.toString() == firmwareVersionUUID.toString() }
+ ?.read()?.decodeToString()?.let {
+ Version.fromString(it)
+ } ?: Version.fromString("0.0.0-0")
+
+ }
+
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readVersion(): Version {
@@ -495,16 +458,10 @@ class BleRepositoryImpl @Inject constructor(
var characteristic = service.findCharacteristic(intervalReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
- val interval = characteristic.readWriteInterval().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
+ val interval = characteristic.readWriteInterval()
val readTimeout = if(version > Version.fromString("0.0.0-0")) {
- characteristic.readTimeoutInterval().fold(
- onFailure = { return Result.failure(it) },
- onSuccess = { it }
- )
+ characteristic.readTimeoutInterval()
} else {
0
}
@@ -582,7 +539,7 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun getHostHistoryBySerial(
serial: String
- ): Flow>, BleException>> {
+ ): Flow>, BleException>> {
return readHostHistory(serial, app)
@@ -720,7 +677,7 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun writeBle(
serial: String,
- request: Ble.Host.WriteRequest
+ request: Ble.Gate.WriteRequest
): Result {
return if(app.checkPermission()) {
diff --git a/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt b/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt
index 3edc796..2cbeea8 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt
@@ -59,7 +59,7 @@ suspend fun readTable(
fun readHostHistory(
address: String,
app: Application,
-): Flow>, BleException>> {
+): Flow>, BleException>> {
return flow {
@@ -85,7 +85,7 @@ fun readHostHistory(
} else {
- val firstTablePackage: MutableList = mutableListOf()
+ val dataTablePackage: MutableList = mutableListOf()
val secondTablePackage: MutableList = mutableListOf()
var tableSize = value.get2byteUIntAt(0)
@@ -93,13 +93,13 @@ fun readHostHistory(
//Чтение без удаления
characteristic.write(DataByteArray.from(1, 0, 0, -1, -1))
val firstTableHeader = characteristic.read().value.asList()
- firstTablePackage.addAll(firstTableHeader.subList(4, firstTableHeader.size))
+ dataTablePackage.addAll(firstTableHeader.subList(4, firstTableHeader.size))
secondTablePackage.addAll(
readTable(characteristic, byteArrayOf(6), byteArrayOf(6))
)
- firstTablePackage.addAll(
+ dataTablePackage.addAll(
readTable(
characteristic,
byteArrayOf(5),
@@ -107,11 +107,11 @@ fun readHostHistory(
)
)
- Log.d("table", firstTablePackage.joinToString { it.toString() })
+ Log.d("table", dataTablePackage.joinToString { it.toString() })
- val bleMeasureInterval = firstTablePackage.toByteArray().get4byteUIntAt(0).toLong()
- val bleLastMeasureTime = firstTablePackage.toByteArray().get4byteUIntAt(4).toLong()
- val bleRealTime = firstTablePackage.toByteArray().get4byteUIntAt(8).toLong()
+ val bleMeasureInterval = dataTablePackage.toByteArray().get4byteUIntAt(0).toLong()
+ val bleLastMeasureTime = dataTablePackage.toByteArray().get4byteUIntAt(4).toLong()
+ val bleRealTime = dataTablePackage.toByteArray().get4byteUIntAt(8).toLong()
val lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
Log.i("BLEK-LOG", "Вермя последнего: ${bleLastMeasureTime} Реальное время: ${bleRealTime}")
@@ -119,7 +119,6 @@ fun readHostHistory(
fun getBleIdIndex(bytes: ByteArray): UInt{
val bits = BitSet.valueOf(bytes)
- Log.d("bits", bits.toByteArray().joinToString())
bits.clear(12, 16)
val arr = bits.toByteArray()
@@ -188,7 +187,7 @@ fun readHostHistory(
do {
val bleIdTableCell =
- firstTablePackage.drop(bleTableOffset).take(2).toByteArray()
+ dataTablePackage.drop(bleTableOffset).take(2).toByteArray()
val intervalEnd = "ff0f"
val intervalEndAndHit = "fe0f"
@@ -232,22 +231,23 @@ fun readHostHistory(
bleTableOffset += 2
if (devDataSize != 0) {
- val payload = getBleIdIndex(
- firstTablePackage.drop(bleTableOffset).take(devDataSize)
+ val payload = //getBleIdIndex(
+ dataTablePackage.drop(bleTableOffset).take(devDataSize)
.toByteArray()
- )
+ //)
+ Log.d("payload", "${payload.joinToString(separator = " ")} ${payload.toHexString()}")
bleTableOffset += devDataSize
}
periodBle.add(serial)
- } while (bleTableOffset < firstTablePackage.size)
+ } while (bleTableOffset < dataTablePackage.size)
emit(
Result.success(
ProgressState.Finished(
periods.withIndex().map {
- Ble.Host.HistoryPoint(
+ Ble.Gate.HistoryPoint(
date = lastMeasureSystemTime - (((periods.size - 1) - it.index) * bleMeasureInterval),
hit = it.value.first,
value = it.value.second
diff --git a/app/src/main/java/llc/arma/ble/data/repository/XlsxRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/repository/XlsxRepositoryImpl.kt
index 85881d4..44ba7c9 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/XlsxRepositoryImpl.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/XlsxRepositoryImpl.kt
@@ -105,7 +105,7 @@ class XlsxRepositoryImpl @Inject constructor(
return mailFile
}
- override fun exportHostDataToXls(bleName: String, data: List): File {
+ override fun exportHostDataToXls(bleName: String, data: List): File {
val fileNameDateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm")
val fileName = "${fileNameDateFormat.format(Date(System.currentTimeMillis()))}.xlsx"
diff --git a/app/src/main/java/llc/arma/ble/domain/common/ProgressState.kt b/app/src/main/java/llc/arma/ble/domain/common/ProgressState.kt
index d9bc9e3..d030957 100644
--- a/app/src/main/java/llc/arma/ble/domain/common/ProgressState.kt
+++ b/app/src/main/java/llc/arma/ble/domain/common/ProgressState.kt
@@ -2,7 +2,7 @@ package llc.arma.ble.domain.common
sealed class ProgressState {
- object Indeterminate : ProgressState()
+ data object Indeterminate : ProgressState()
data class Progress(
val value: Float
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 277f1d6..e964dd7 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
@@ -112,10 +112,10 @@ sealed class Ble(
}
- class Host(
+ class Gate(
info: BleInfo,
state: BleState,
- val hostState: HostState
+ val gateState: HostState
) : Ble(info, state){
class HistoryPoint(
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 0195047..c0d8122 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
@@ -2,8 +2,10 @@ package llc.arma.ble.domain.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
import llc.arma.ble.data.repository.BleRepositoryImpl
+@Serializable
@Parcelize
data class BleInfo(
val name: String,
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 6a35b95..00850b5 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
@@ -1,5 +1,6 @@
package llc.arma.ble.domain.repository
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
@@ -16,9 +17,16 @@ interface BleRepository {
fun getFoundBle(): List
- fun getBleAroundFlow(): Result>, BleException>
+ fun getBleAroundFlow(): Flow>
- suspend fun getBleBySerial(serial: String) : Result, BleException>
+ suspend fun getBleInfoFlow(
+ serial: String
+ ): Flow
+
+ suspend fun getBleBySerial(
+ serial: String,
+ scope: CoroutineScope
+ ) : Result
suspend fun getTemperatureHistoryBySerial(
serial: String
@@ -36,7 +44,7 @@ interface BleRepository {
suspend fun writeBle(
serial: String,
- request: Ble.Host.WriteRequest
+ request: Ble.Gate.WriteRequest
): Result
suspend fun writeBle(
@@ -73,7 +81,7 @@ interface BleRepository {
suspend fun getHostHistoryBySerial(
serial: String
- ): Flow>, BleException>>
+ ): Flow>, BleException>>
suspend fun getHostBleTableBySerial(
serial: String
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
index 72eefb2..a6e9958 100644
--- a/app/src/main/java/llc/arma/ble/domain/repository/XlsxRepository.kt
+++ b/app/src/main/java/llc/arma/ble/domain/repository/XlsxRepository.kt
@@ -12,7 +12,7 @@ interface XlsxRepository {
fun exportHostDataToXls(
bleName: String,
- data: List
+ 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
index ebadbe9..5c81eb1 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/ExportToXlsx.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/ExportToXlsx.kt
@@ -23,7 +23,7 @@ class ExportToXlsx @Inject constructor(
@JvmName("invokeHost")
operator fun invoke(
bleName: String,
- data: List
+ data: List
){
val file = xlsxRepository.exportHostDataToXls(bleName, data)
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleAroundFlow.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleAroundFlow.kt
index 03e272a..208d65d 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleAroundFlow.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleAroundFlow.kt
@@ -11,6 +11,6 @@ class GetBleAroundFlow @Inject constructor(
private val bleRepository: BleRepository
) {
- operator fun invoke(): Result>, BleException> = bleRepository.getBleAroundFlow()
+ operator fun invoke() = bleRepository.getBleAroundFlow()
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt
index 5dc27f3..7d1be66 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleBySerial.kt
@@ -1,5 +1,7 @@
package llc.arma.ble.domain.usecase
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
@@ -11,8 +13,14 @@ class GetBleBySerial @Inject constructor(
private val bleRepository: BleRepository
) {
- suspend operator fun invoke(serial: String): Result, BleException> =
- bleRepository.getBleBySerial(serial)
+ suspend operator fun invoke(
+ serial: String,
+ scope: CoroutineScope
+ ): Result {
+
+ return bleRepository.getBleBySerial(serial, scope)
+
+ }
sealed class GetBleException {
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt
index 109cf57..11b05f4 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt
@@ -12,7 +12,7 @@ class GetHostHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository,
) {
- suspend operator fun invoke(serial: String): Flow>, BleException>> {
+ suspend operator fun invoke(serial: String): Flow>, BleException>> {
return bleRepository.getHostHistoryBySerial(serial)
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 381797d..24e4c56 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
@@ -25,7 +25,7 @@ class WriteBle @Inject constructor(
suspend operator fun invoke(
serial: String,
- request: Ble.Host.WriteRequest
+ request: Ble.Gate.WriteRequest
): llc.arma.ble.domain.Result{
return bleRepository.writeBle(serial, request)
}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 7e546d0..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,22 +0,0 @@
-buildscript {
- ext {
- compose_version = '1.3.3'
- kotlin_version = '1.9.22'
- }
-
- dependencies {
- classpath('com.google.dagger:hilt-android-gradle-plugin:2.46')
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10"
- }
- repositories {
- mavenCentral()
- }
-
-}// Top-level build file where you can add configuration options common to all sub-projects/modules.
-plugins {
- id 'com.android.application' version '8.8.1' apply false
- id 'com.android.library' version '8.8.1' apply false
- id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
- id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22'
- id("androidx.room") version "2.6.1" apply false
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..7bd38db
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+
+ alias(libs.plugins.room) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
+}
\ No newline at end of file
diff --git a/common/.gitignore b/common/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/common/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
new file mode 100644
index 0000000..011661c
--- /dev/null
+++ b/common/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+ namespace = "llc.arma.common"
+ compileSdk = 35
+
+ defaultConfig {
+ minSdk = 24
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+
+ implementation(libs.scanner)
+ implementation(libs.client)
+
+}
\ No newline at end of file
diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/common/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/common/src/androidTest/java/llc/arma/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/llc/arma/common/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..7f90ab6
--- /dev/null
+++ b/common/src/androidTest/java/llc/arma/common/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package llc.arma.common
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("llc.arma.common.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a8519e5
--- /dev/null
+++ b/common/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/BleException.kt b/common/src/main/java/llc/arma/common/BleException.kt
new file mode 100644
index 0000000..cb4aa1e
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/BleException.kt
@@ -0,0 +1,7 @@
+package llc.arma.common
+
+sealed class BleException {
+
+ data object UnexpectedResponse : BleException()
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/GetAccelerometerHistory.kt b/common/src/main/java/llc/arma/common/GetAccelerometerHistory.kt
new file mode 100644
index 0000000..169c99f
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/GetAccelerometerHistory.kt
@@ -0,0 +1,249 @@
+package llc.arma.common
+
+import android.Manifest
+import android.app.Application
+import android.util.Log
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.ProgressState
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.fromByte
+import llc.arma.common.extensions.get2byteShortAt
+import llc.arma.common.extensions.get2byteUIntAt
+import llc.arma.common.extensions.get4byteUIntAt
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
+
+sealed class BleCommand(
+ val command: ByteArray
+) {
+
+ sealed class Common(
+ command: ByteArray
+ ) : BleCommand(command){
+
+ data object WriteToFlash : Common(byteArrayOf(9))
+
+ }
+
+ sealed class Host(
+ command: ByteArray
+ ) : BleCommand(command) {
+
+ data object ReadSettings : Accelerometer(byteArrayOf(4))
+
+ data object ReadHistoryTableSize : Accelerometer(byteArrayOf(2))
+
+ class InitReadHistory(
+ count: ByteArray
+ ) : Accelerometer(byteArrayOf(1, 0, 0) + count)
+
+ data object ContinueReadHistory : Accelerometer(byteArrayOf(5))
+
+ }
+
+ sealed class Accelerometer(
+ command: ByteArray
+ ) : BleCommand(command) {
+
+ data object ReadSettings : Accelerometer(byteArrayOf(4))
+
+ data object ReadHistoryTableSize : Accelerometer(byteArrayOf(2))
+
+ class InitReadHistory(
+ count: ByteArray
+ ) : Accelerometer(byteArrayOf(1, 0, 0) + count)
+
+ data object ContinueReadHistory : Accelerometer(byteArrayOf(5))
+
+ }
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+suspend fun ClientBleGattCharacteristic.sendCommand(
+ command: BleCommand
+) = write(DataByteArray.from(*command.command))
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+suspend fun ClientBleGattCharacteristic.sendReadCommand(
+ command: BleCommand
+): DataByteArray {
+ write(DataByteArray.from(*command.command))
+ return read()
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalUnsignedTypes::class)
+fun getAccelerometerHistory(
+ address: String,
+ app: Application,
+): Flow>, BleException>> {
+
+ return flow {
+
+ val resultPackage: MutableList = mutableListOf()
+ val result = mutableListOf>()
+
+ val connection = ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val specData = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(accelerometerReadUUID)
+ ?.sendReadCommand(BleCommand.Accelerometer.ReadSettings)
+ ?: throw IllegalStateException()
+
+ val scale = AccelScale.fromByte(specData.value[1]) ?: throw IllegalStateException()
+ val mode = AccelViewMode.fromByte(specData.value[0]) ?: throw IllegalStateException()
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(accelerometerHistoryReadUUID)
+
+ if (characteristic != null) {
+
+ var value = characteristic.sendReadCommand(BleCommand.Accelerometer.ReadHistoryTableSize).value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ emit(Result.success(ProgressState.Finished(emptyList())))
+
+ } else {
+
+ var lastMeasureSystemTime: Long? = null
+ var bleMeasureInterval: Long? = null
+
+ var nextPackageDataCount = value.get2byteUIntAt(0)
+
+ value = characteristic.sendReadCommand(BleCommand.Accelerometer.InitReadHistory(
+ value.toList().take(2).toByteArray()
+ )).value
+
+ while (nextPackageDataCount.toInt() != 0) {
+
+ val packageDataArray = if (value[0] == 250.toByte()) {
+
+ bleMeasureInterval = value.get4byteUIntAt(4).toLong()
+ val bleLastMeasureTime = value.get4byteUIntAt(8).toLong()
+ val bleRealTime = value.get4byteUIntAt(12).toLong()
+
+ lastMeasureSystemTime =
+ System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
+
+ value.toUByteArray().asList().subList(16, value.size)
+
+ } else {
+
+ value.toUByteArray().asList().subList(4, value.size)
+
+ }
+
+ result.add(value.toUByteArray().toList())
+ nextPackageDataCount = value.get2byteUIntAt(2)
+
+ resultPackage.addAll(
+ packageDataArray.chunked(2).map {
+ it.toUByteArray().toByteArray().get2byteShortAt().toFloat()
+ }.toMutableList()
+ )
+
+ val expectedDataSize = nextPackageDataCount.toInt() + resultPackage.size
+
+ emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
+ emit(Result.success(ProgressState.Progress(resultPackage.size.toFloat() / expectedDataSize.toFloat())))
+
+ value = characteristic.sendReadCommand(BleCommand.Accelerometer.ContinueReadHistory).value
+
+ }
+
+ emit(
+ Result.success(
+ ProgressState.Finished(
+ when (mode) {
+ AccelViewMode.ROTATIONS -> {
+ resultPackage.withIndex().map {
+ Ble.Accelerometer.HistoryPoint.Rotation(
+ date = lastMeasureSystemTime!! - (((resultPackage.size - 1) - it.index) * bleMeasureInterval!!),
+ value = it.value.toLong()
+ )
+ }
+ }
+
+ AccelViewMode.ACCELERATION,
+ AccelViewMode.PEAK_ACCELERATION,
+ AccelViewMode.RMS -> {
+ resultPackage.chunked(3).withIndex().map {
+ Ble.Accelerometer.HistoryPoint.Angle(
+ date = lastMeasureSystemTime!! - (((resultPackage.size / 3 - 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 -> {
+ resultPackage.chunked(3).withIndex().map {
+ Ble.Accelerometer.HistoryPoint.Angle(
+ date = lastMeasureSystemTime!! - (((resultPackage.size / 3 - 1) - it.index) * bleMeasureInterval!!),
+ x = calculateAngle(
+ it.value[2],
+ it.value[1]
+ ),
+ y = calculateAngle(
+ it.value[2],
+ it.value[0]
+ ),
+ z = calculateAngle(
+ it.value[0],
+ it.value[1]
+ )
+ )
+ }
+ }
+
+ AccelViewMode.VIBRATION -> {
+ resultPackage.withIndex().map {
+ Ble.Accelerometer.HistoryPoint.Vibration(
+ date = lastMeasureSystemTime!! - (((resultPackage.size - 1) - it.index) * bleMeasureInterval!!),
+ value = (it.value * scale.k) / Short.MAX_VALUE
+ )
+ }
+ }
+ }
+ )
+ )
+
+ )
+
+ }
+
+
+ } else {
+
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ }
+
+ } catch (err: Throwable) {
+
+ err.printStackTrace()
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ } finally {
+
+ Log.d("connection", "finally")
+ connection.close()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/GetAccelerometerRealtimeData.kt b/common/src/main/java/llc/arma/common/GetAccelerometerRealtimeData.kt
new file mode 100644
index 0000000..689be11
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/GetAccelerometerRealtimeData.kt
@@ -0,0 +1,125 @@
+package llc.arma.common
+
+import android.app.Application
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.get2byteShortAt
+import llc.arma.common.extensions.sendData
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+
+@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+fun getAccelerometerRealtimeData(
+ app: Application,
+ serial: String,
+ accelScale: AccelScale,
+ accelMode: AccelViewMode,
+ fftAxis: FftAxis,
+ fftMode: FftViewMode,
+ frequency: FftFrequency,
+): Flow> {
+
+ return flow {
+
+ val connection =
+ ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val services = connection.discoverServices()
+
+ val characteristic =
+ services.findService(serviceUUID)
+ ?.findCharacteristic(accelerometerReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(
+ DataByteArray.from(
+ 4,
+ accelMode.sendData,
+ accelScale.sendData,
+ fftMode.sendData,
+ fftAxis.sendData,
+ frequency.sendData,
+ 1
+ )
+ )
+
+ characteristic.getNotifications().collect {
+
+ val value = it.value
+
+ val data = value.toList().chunked(2).map {
+ it.toByteArray().get2byteShortAt()
+ }
+
+ val result = when (accelMode) {
+ AccelViewMode.VIBRATION -> {
+ Ble.Accelerometer.RealtimePoint.Vibration(
+ (value.get2byteShortAt()
+ .toFloat() * accelScale.k) / Short.MAX_VALUE
+ )
+ }
+
+ AccelViewMode.ANGLE -> {
+ Ble.Accelerometer.RealtimePoint.Angle(
+ x = calculateAngle(
+ data[2].toFloat(),
+ data[1].toFloat()
+ ),
+ y = calculateAngle(
+ data[2].toFloat(),
+ data[0].toFloat()
+ ),
+ z = calculateAngle(
+ data[0].toFloat(),
+ data[1].toFloat()
+ )
+ )
+ }
+
+ AccelViewMode.ROTATIONS -> {
+ Ble.Accelerometer.RealtimePoint.Rotation(
+ angle = ((360f / 8f) * ((data[0] / 100f) + 1f)) - 45f,
+ tmp = data[1].toFloat(),
+ turnovers = data[2]
+ )
+ }
+
+ AccelViewMode.ACCELERATION,
+ AccelViewMode.PEAK_ACCELERATION,
+ AccelViewMode.RMS -> {
+ Ble.Accelerometer.RealtimePoint.Common(
+ 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,
+ )
+ }
+ }
+
+ emit(Result.success(result))
+
+ }
+
+
+ } catch (err: Exception) {
+
+ err.printStackTrace()
+
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ } finally {
+
+ connection.disconnect()
+ connection.close()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/GetBleAroundFlow.kt b/common/src/main/java/llc/arma/common/GetBleAroundFlow.kt
new file mode 100644
index 0000000..7b36c6b
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/GetBleAroundFlow.kt
@@ -0,0 +1,71 @@
+package llc.arma.common
+
+import android.app.Application
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import llc.arma.common.domain.BleInfo
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleNumOfMatches
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerCallbackType
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerMatchMode
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
+import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
+import java.util.Collections
+import java.util.Timer
+import java.util.TimerTask
+import llc.arma.common.extensions.info
+
+@RequiresPermission(allOf = [
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_CONNECT
+])
+fun getBleAroundFlow(
+ app: Application
+): Flow>{
+
+ val resultList: MutableMap =
+ Collections.synchronizedMap(mutableMapOf())
+
+ return callbackFlow {
+
+ val job = BleScanner(app)
+ .scan(
+ settings = BleScannerSettings(
+ includeStoredBondedDevices = false,
+ scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
+ callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
+ matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
+ numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
+ reportDelay = 0L
+ )
+ ).filter { it.device.name?.contains("ArmA") == true }
+ .onEach {
+ resultList[it.device.address] = it.info
+ }.launchIn(CoroutineScope(Dispatchers.IO))
+
+ val timer = Timer().apply {
+ schedule(object : TimerTask() {
+ override fun run() {
+ CoroutineScope(Dispatchers.IO).launch {
+ send(resultList.values.toList())
+ }
+ }
+ }, 500, 500)
+ }
+
+ awaitClose {
+ job.cancel()
+ timer.cancel()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/GetBleBySerial.kt b/common/src/main/java/llc/arma/common/GetBleBySerial.kt
new file mode 100644
index 0000000..79668aa
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/GetBleBySerial.kt
@@ -0,0 +1,318 @@
+package llc.arma.common
+
+import android.Manifest
+import android.app.Application
+import android.os.SystemClock
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.BleInfo
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+import llc.arma.common.domain.Result
+import llc.arma.common.domain.Version
+import llc.arma.common.extensions.fromByte
+import llc.arma.common.extensions.get4byteUIntAt
+import llc.arma.common.extensions.toTemperature
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+private suspend fun ClientBleGatt.readVersion(): Version {
+
+ return discoverServices().findService(versionServiceUUID)
+ ?.findCharacteristic(firmwareVersionUUID)?.read()?.value?.decodeToString()?.let {
+ Version.fromString(it)
+ } ?: Version.fromString("0.0.0-0")
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+private suspend fun ClientBleGattCharacteristic.readWriteInterval(
+
+): Result {
+
+ write(DataByteArray.from(3, 0, 0, 0))
+
+ val readTimeout = read().value.let {
+ if(it.size == 4){
+ it.get4byteUIntAt(0).toLong()
+ } else {
+ 0
+ }
+ }
+
+ return Result.success(readTimeout)
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+private suspend fun ClientBleGatt.readHostState(
+
+): Result {
+
+ val service = discoverServices().findService(serviceUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ val characteristic = service.findCharacteristic(intervalReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ val interval = characteristic.readWriteInterval().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ val readTimeout = characteristic.readTimeoutInterval().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ return Result.success(
+ Ble.Host.HostState(
+ historyInterval = interval,
+ readInterval = readTimeout
+ )
+ )
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+private suspend fun ClientBleGattCharacteristic.readTimeoutInterval(
+
+): Result {
+
+ write(DataByteArray.from(8, 0, 0, 0))
+
+ val interval = read().value.let {
+ if (it.size == 4) {
+ it.get4byteUIntAt(0).toLong()
+ } else {
+ 0
+ }
+ }
+
+ return Result.success(interval)
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalUnsignedTypes::class)
+private suspend fun ClientBleGatt.readThermometerState(
+ timer: BleInfo.HistoryTableStatus
+): Result {
+
+ val service = discoverServices().findService(serviceUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ var characteristic = service.findCharacteristic(temperatureReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ characteristic.write(DataByteArray.from(1, 1))
+
+ delay(2_000)
+
+ val temperature = characteristic.read().value.toUByteArray().toTemperature()
+
+ characteristic = service.findCharacteristic(intervalReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ val interval = characteristic.readWriteInterval().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ return Result.success(
+ Ble.Thermometer.ThermometerState(
+ temperature = temperature,
+ saveHistory = timer !== BleInfo.HistoryTableStatus.DISABLED,
+ historyInterval = interval
+ )
+ )
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+private suspend fun ClientBleGatt.readAccelState(
+ timer: BleInfo.HistoryTableStatus,
+ version: Version
+): Result {
+
+ val services = discoverServices()
+ val service = services.findService(serviceUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ var characteristic = service.findCharacteristic(intervalReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ val interval = characteristic.readWriteInterval().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ val readTimeout = if(version > Version.fromString("0.0.0-0")) {
+ characteristic.readTimeoutInterval().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+ } else {
+ 0
+ }
+
+ val historySettingsParams = when (timer) {
+ BleInfo.HistoryTableStatus.EMPTY,
+ BleInfo.HistoryTableStatus.NOT_EMPTY -> {
+
+ characteristic = service.findCharacteristic(accelerometerReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ characteristic.write(DataByteArray.from(4))
+ characteristic.read().let {
+
+ val data = it.value
+
+ val scale = AccelScale.fromByte(data[1]) ?: return Result.failure(
+ BleException.UnexpectedResponse
+ )
+ val mode = AccelViewMode.fromByte(data[0]) ?: return Result.failure(
+ BleException.UnexpectedResponse
+ )
+
+ Ble.Accelerometer.HistorySettings.Enabled(
+ scale = scale,
+ mode = mode,
+ detailed = false
+ )
+ }
+ }
+
+ BleInfo.HistoryTableStatus.DISABLED -> Ble.Accelerometer.HistorySettings.Disabled
+ }
+
+ return Result.success(
+ Ble.Accelerometer.AccelerometerState(
+ saveHistorySettings = historySettingsParams,
+ historyInterval = interval,
+ readInterval = readTimeout
+ )
+ )
+
+}
+
+@RequiresPermission(allOf = [
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT
+])
+suspend fun getBleBySerial(
+ app: Application,
+ serial: String
+): Result {
+
+ val initialBle = getBleInfoBySerial(app, serial).getOrNull()
+
+ return if(initialBle == null){
+
+ Result.failure(BleException.UnexpectedResponse)
+
+ } else {
+
+ val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.IO))
+
+ try {
+
+ fun BleInfo.updateBleInfo(): BleInfo {
+ return copy(
+ rssi = if ((SystemClock.elapsedRealtime() - scanTime) > 15_000) {
+ null
+ } else {
+ rssi
+ }
+ )
+ }
+
+ val version = connection.readVersion()
+
+ fun BleInfo.updateState(): Ble.BleState {
+ return Ble.BleState(
+ tx = Ble.BleState.TX.fromByte(tx.toByte())
+ ?: Ble.BleState.TX.ZERO,
+ version = version
+ )
+ }
+
+ val firstResult =
+ when (initialBle.type) {
+ BleInfo.Type.HOST -> {
+
+ val tState = connection.readHostState().fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ Ble.Host(
+ info = initialBle.updateBleInfo(),
+ state = initialBle.updateState(),
+ hostState = tState
+ )
+
+ }
+
+ BleInfo.Type.BEACON -> {
+ Ble.Beacon(
+ info = initialBle.updateBleInfo(),
+ state = initialBle.updateState(),
+ )
+ }
+
+ BleInfo.Type.THERMOMETER -> {
+
+ val tState = connection.readThermometerState(
+ initialBle.tableStatus
+ ).fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ Ble.Thermometer(
+ info = initialBle.updateBleInfo(),
+ state = initialBle.updateState(),
+ thermometerState = tState
+ )
+
+ }
+
+ BleInfo.Type.ACCELEROMETER -> {
+
+ val tState = connection.readAccelState(
+ initialBle.tableStatus,
+ version
+ ).fold(
+ onFailure = { return Result.failure(it) },
+ onSuccess = { it }
+ )
+
+ Ble.Accelerometer(
+ info = initialBle.updateBleInfo(),
+ state = initialBle.updateState(),
+ accelerometerState = tState
+ )
+
+ }
+
+ }
+
+ Result.success(firstResult)
+
+ } catch (err: Throwable) {
+ err.printStackTrace()
+ return Result.failure(BleException.UnexpectedResponse)
+ } finally {
+
+ connection.close()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/GetBleInfoBySerial.kt b/common/src/main/java/llc/arma/common/GetBleInfoBySerial.kt
new file mode 100644
index 0000000..e8e95b7
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/GetBleInfoBySerial.kt
@@ -0,0 +1,50 @@
+package llc.arma.common
+
+import android.app.Application
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.withTimeoutOrNull
+import llc.arma.common.domain.BleInfo
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleNumOfMatches
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerCallbackType
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerMatchMode
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
+import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.info
+
+@RequiresPermission(allOf = [
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.BLUETOOTH_CONNECT
+])
+suspend fun getBleInfoBySerial(
+ app: Application,
+ serial: String
+): Result {
+
+ val scanner = BleScanner(app)
+ .scan(
+ settings = BleScannerSettings(
+ includeStoredBondedDevices = false,
+ scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
+ callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
+ matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
+ numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
+ reportDelay = 0L
+ )
+ ).filter { it.device.name?.contains("ArmA") == true }
+ .filter { it.info.serial == serial }
+
+ val result = withTimeoutOrNull(30_000){
+ scanner.first().info
+ }
+
+ return if(result == null){
+ Result.failure(BleException.UnexpectedResponse)
+ } else {
+ Result.success(result)
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/ReadAccelerometerSpectre.kt b/common/src/main/java/llc/arma/common/ReadAccelerometerSpectre.kt
new file mode 100644
index 0000000..54f4d75
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/ReadAccelerometerSpectre.kt
@@ -0,0 +1,179 @@
+package llc.arma.common
+
+import android.app.Application
+import android.bluetooth.BluetoothGattDescriptor
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.ProgressState
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.get2byteShortAt
+import llc.arma.common.extensions.get2byteUIntAt
+import llc.arma.common.extensions.get4byteUIntAt
+import llc.arma.common.extensions.sendData
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+import java.util.UUID
+
+@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+fun readAccelerometerSpectre(
+ address: String,
+ app: Application,
+ accelScale: AccelScale,
+ accelMode: AccelViewMode,
+ fftAxis: FftAxis,
+ fftMode: FftViewMode,
+ frequency: FftFrequency,
+): Flow>, BleException>> {
+
+ return flow {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val service = connection.discoverServices()
+ .findService(serviceUUID) ?: throw IllegalStateException()
+
+ val characteristic = service.findCharacteristic(accelerometerReadUUID)
+ ?: throw IllegalStateException()
+
+ val historyCharacteristic = service.findCharacteristic(accelerometerHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.findDescriptor(
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
+ )?.write(DataByteArray(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE))
+ ?: throw IllegalStateException()
+
+ characteristic.write(
+ DataByteArray(
+ byteArrayOf(
+ 4,
+ accelMode.sendData,
+ accelScale.sendData,
+ fftMode.sendData,
+ fftAxis.sendData,
+ frequency.sendData,
+ 2
+ )
+ )
+ )
+
+ characteristic.getNotifications().collect {
+
+ var initialValue: Long? = null
+ var frequencyInterval: Long? = null
+
+ val resultAccelerometerPackage: MutableList = mutableListOf()
+
+ var expectedDataSize: Int? = null
+
+ historyCharacteristic.write(DataByteArray.from(2))
+
+ var value = historyCharacteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ emit(Result.success(ProgressState.Finished(emptyList())))
+
+ } else {
+
+ var nextPackageDataCount = value.get2byteUIntAt(0)
+
+ val writeData = mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray()
+
+ historyCharacteristic.write(DataByteArray(writeData))
+ value = historyCharacteristic.read().value
+
+
+ while (nextPackageDataCount.toInt() != 0) {
+
+ val accelerometerDataArray = if (value[0] == 250.toByte()) {
+
+ initialValue = value.get4byteUIntAt(8).toLong()
+ frequencyInterval = value.get4byteUIntAt(4).toLong()
+
+ value.asList().subList(16, value.size)
+
+ } else {
+ value.asList().subList(4, value.size)
+
+ }
+
+ nextPackageDataCount = value.get2byteUIntAt(2)
+
+ resultAccelerometerPackage.addAll(
+ accelerometerDataArray.chunked(2).map {
+ it.toByteArray().get2byteShortAt().toFloat()
+ }.toMutableList()
+ )
+
+ expectedDataSize =
+ nextPackageDataCount.toInt() + resultAccelerometerPackage.size
+
+ emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
+ emit(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize.toFloat())))
+
+ historyCharacteristic.write(DataByteArray.from(5))
+ value = historyCharacteristic.read().value
+
+ }
+
+ emit(
+ Result.success(
+ ProgressState.Finished(
+ resultAccelerometerPackage.withIndex().map {
+ Ble.Accelerometer.SpectrePoint(
+ frequency = frequencyInterval!! * it.index + initialValue!!,
+ value = (it.value * accelScale.k) / Short.MAX_VALUE
+ )
+ }
+ )
+ )
+
+ )
+
+ characteristic.write(
+ DataByteArray(
+ byteArrayOf(
+ 4,
+ accelMode.sendData,
+ accelScale.sendData,
+ fftMode.sendData,
+ fftAxis.sendData,
+ frequency.sendData,
+ 2
+ )
+ )
+ )
+
+ }
+
+ }
+
+
+ } catch (err: Throwable) {
+
+ err.printStackTrace()
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/ReadHostHistory.kt b/common/src/main/java/llc/arma/common/ReadHostHistory.kt
new file mode 100644
index 0000000..e585a37
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/ReadHostHistory.kt
@@ -0,0 +1,392 @@
+package llc.arma.common
+
+import android.Manifest
+import android.app.Application
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.ProgressState
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.get2byteUIntAt
+import llc.arma.common.extensions.get4byteUIntAt
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
+import java.nio.ByteBuffer
+import java.util.BitSet
+import java.util.Locale
+
+@RequiresPermission(allOf = ["android.permission.BLUETOOTH_CONNECT"])
+suspend fun readTable(
+ characteristic: ClientBleGattCharacteristic,
+ startRequest: ByteArray,
+ nextRequestPayload: ByteArray
+): List {
+
+ characteristic.write(DataByteArray(startRequest))
+
+ var value = characteristic.read().value
+ val tableResult = mutableListOf()
+
+ do {
+
+ val nextPackageDataCount = if(value.size > 3)
+ value.get2byteUIntAt(2)
+ else
+ UInt.MIN_VALUE
+
+ if(value.size > 4) {
+
+ tableResult.addAll(value.asList().subList(4, value.size))
+
+ characteristic.write(DataByteArray(nextRequestPayload))
+ value = characteristic.read().value
+
+ }
+
+ } while (nextPackageDataCount.toInt() != 0)
+
+ return tableResult
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalStdlibApi::class)
+fun readHostHistory(
+ address: String,
+ app: Application,
+): Flow>, BleException>> {
+
+ return flow {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(hostHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(DataByteArray.from(2))
+
+ val value = characteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ emit(Result.success(ProgressState.Finished(emptyList())))
+
+ } else {
+
+ val dataTable: MutableList = mutableListOf()
+ val bleIdTable: MutableList = mutableListOf()
+
+ //Чтение без удаления
+ characteristic.write(DataByteArray.from(1, 0, 0, -1, -1))
+ val firstTableHeader = characteristic.read().value.asList()
+ dataTable.addAll(firstTableHeader.subList(4, firstTableHeader.size))
+
+ bleIdTable.addAll(readTable(characteristic, byteArrayOf(6), byteArrayOf(6)))
+ dataTable.addAll(readTable(characteristic, byteArrayOf(5), byteArrayOf(5)))
+
+ val bleMeasureInterval = dataTable.toByteArray().get4byteUIntAt(0).toLong()
+ val bleLastMeasureTime = dataTable.toByteArray().get4byteUIntAt(4).toLong()
+ val bleRealTime = dataTable.toByteArray().get4byteUIntAt(8).toLong()
+
+ val lastMeasureSystemTime =
+ System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
+
+ fun getBleIdIndex(bytes: ByteArray): UInt {
+
+ val bits = BitSet.valueOf(bytes)
+ bits.clear(12, 16)
+
+ val arr = bits.toByteArray()
+
+ if (arr.isEmpty()) {
+ return 0x00.toUInt()
+ }
+
+ if (arr.size == 1) {
+ return arr[0].toUInt()
+ }
+
+ return arr.get2byteUIntAt(0)
+
+ }
+
+ fun getInnerIndex(byte: Byte): Int {
+
+ var bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(0, 4)
+ bits = bits.get(4, 8)
+ val arr = bits.toByteArray()
+
+ if (arr.isEmpty()) {
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ fun getDevType(byte: Byte): Int {
+
+ val bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(5, 9)
+ val arr = bits.toByteArray()
+
+ if (arr.isEmpty()) {
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ fun getDevDataSize(byte: Byte): Int {
+
+ var bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(0, 5)
+ bits = bits.get(5, 8)
+ val arr = bits.toByteArray()
+
+ if (arr.isEmpty()) {
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ var bleTableOffset = 12
+ val periods = mutableListOf>>>()
+
+ var periodBle = mutableListOf>()
+
+ do {
+
+ val bleIdTableCell =
+ dataTable.drop(bleTableOffset).take(2).toByteArray()
+
+ val intervalEnd = "ff0f"
+ val intervalEndAndHit = "fe0f"
+
+ if (bleIdTableCell.contentEquals(intervalEnd.hexToByteArray())) {
+
+ bleTableOffset += 2
+ if (periodBle.isEmpty()) bleTableOffset += 2
+ periods.add(Pair(false, periodBle))
+ periodBle = mutableListOf()
+
+ continue
+ }
+
+ if (bleIdTableCell.contentEquals(intervalEndAndHit.hexToByteArray())) {
+ bleTableOffset += 2
+ if (periodBle.isEmpty()) bleTableOffset += 2
+ periods.add(Pair(true, periodBle))
+ periodBle = mutableListOf()
+
+ continue
+ }
+
+ val innerIndex = getInnerIndex(bleIdTableCell[1])
+
+ val bleTableIndex = getBleIdIndex(bleIdTableCell) * 8u
+
+ val serial =
+ bleIdTable.drop(bleTableIndex.toInt()).take(6).reversed()
+ .joinToString(
+ separator = ":",
+ transform = { it.toHexString().padStart(2, '0') })
+ .uppercase(Locale.getDefault())
+ val devTypeByte = bleIdTable.drop(bleTableIndex.toInt() + 6)[0]
+
+ val devType = getDevType(devTypeByte)
+ val devDataSize = getDevDataSize(devTypeByte)
+ bleTableOffset += 2
+
+ if (innerIndex == 0)
+ bleTableOffset += 2
+
+
+ val rssi = if (devDataSize != 0) {
+ dataTable.drop(bleTableOffset).take(devDataSize)
+ .toByteArray()[0].toInt()
+ } else {
+ null
+ }
+
+ bleTableOffset += devDataSize
+
+ periodBle.add(Pair(serial, rssi))
+
+ } while (bleTableOffset < dataTable.size)
+
+ emit(
+ Result.success(
+ ProgressState.Finished(
+ periods.withIndex().map {
+ Ble.Host.HistoryPoint(
+ date = lastMeasureSystemTime - (((periods.size - 1) - it.index) * bleMeasureInterval),
+ hit = it.value.first,
+ value = it.value.second.map {
+ Ble.Host.HistoryPoint.Device(
+ serial = it.first,
+ rssi = it.second
+ )
+ }
+ )
+ }
+ )
+ )
+
+ )
+
+ }
+
+
+ } catch (err: Throwable) {
+ err.printStackTrace()
+ try {
+ emit(Result.failure(BleException.UnexpectedResponse))
+ } catch (_: Throwable) { }
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ }
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalStdlibApi::class)
+suspend fun readHostBleTable(
+ address: String,
+ app: Application,
+): Result, BleException> {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ return try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(hostHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(DataByteArray.from(7))
+
+ var value = characteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ Result.success(emptyList())
+
+ } else {
+
+ val writeData = mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray()
+
+ characteristic.write(DataByteArray(writeData))
+ value = characteristic.read().value
+
+ Result.success(
+ readTable(characteristic, byteArrayOf(6), byteArrayOf(6)).chunked(8).map {
+ it.take(6)
+ .reversed()
+ .joinToString(
+ separator = ":",
+ transform = { it.toHexString().padStart(2, '0') })
+ .uppercase(Locale.getDefault())
+ }
+ )
+
+ }
+
+
+ } catch (err: Throwable) {
+
+ Result.failure(BleException.UnexpectedResponse)
+
+ } finally {
+
+ connection.close()
+
+ }
+
+
+}
+
+@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalStdlibApi::class)
+suspend fun editBleHostTable(
+ address: String,
+ addBleAddress: List,
+ app: Application,
+): Result {
+
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ return try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(flashWriteUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(DataByteArray.from(12, 1))
+
+ val writeCount = addBleAddress.chunked(20).sumOf { bleAddressBatch ->
+
+ val countPayload =
+ ByteBuffer.allocate(2).putShort(bleAddressBatch.size.toShort()).array().reversed()
+ .toByteArray()
+
+ val command = "0b00".hexToByteArray()
+
+ val serialPayload = bleAddressBatch.flatMap {
+ it.replace(":", "").lowercase(Locale.CANADA).hexToByteArray().reversed().toList()
+ }.toByteArray()
+
+ characteristic.write(DataByteArray.from(*command, *countPayload, *serialPayload))
+ characteristic.read().value.get2byteUIntAt(0).toInt()
+
+ }
+
+ characteristic.sendCommand(BleCommand.Common.WriteToFlash)
+
+ delay(10_000)
+
+ Result.success(writeCount)
+
+ } catch (err: Throwable) {
+
+ err.printStackTrace()
+
+ Result.failure(BleException.UnexpectedResponse)
+
+ } finally {
+
+ connection.close()
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/ReadTemperatureHistory.kt b/common/src/main/java/llc/arma/common/ReadTemperatureHistory.kt
new file mode 100644
index 0000000..b01feac
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/ReadTemperatureHistory.kt
@@ -0,0 +1,130 @@
+package llc.arma.common
+
+import android.app.Application
+import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.common.domain.Ble
+import llc.arma.common.domain.ProgressState
+import llc.arma.common.domain.Result
+import llc.arma.common.extensions.get2byteUIntAt
+import llc.arma.common.extensions.get4byteUIntAt
+import llc.arma.common.extensions.toTemperature
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+
+@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+@OptIn(ExperimentalUnsignedTypes::class)
+fun readThermometerHistory(
+ address: String,
+ app: Application,
+): Flow>, BleException>> {
+
+ return flow {
+
+ var lastMeasureSystemTime: Long? = null
+ var bleMeasureInterval: Long? = null
+
+ val resultTemperaturePackage: MutableList = mutableListOf()
+
+ val connection = ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val historyCharacteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(temperatureHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ historyCharacteristic.write(DataByteArray.from(2))
+
+ var value = historyCharacteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ emit(Result.success(ProgressState.Finished(emptyList())))
+
+ } else {
+
+ var nextPackageDataCount = value.get2byteUIntAt(0)
+
+ val writeData = mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray()
+
+ historyCharacteristic.write(DataByteArray(writeData))
+ value = historyCharacteristic.read().value
+
+ while (nextPackageDataCount.toInt() != 0) {
+
+ val temperatureDataArray = if (value[0] == 250.toByte()) {
+
+ bleMeasureInterval = value.get4byteUIntAt(4).toLong()
+ val bleLastMeasureTime = value.get4byteUIntAt(8).toLong()
+ val bleRealTime = value.get4byteUIntAt(12).toLong()
+
+ lastMeasureSystemTime =
+ System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
+
+ value.toUByteArray().asList().subList(16, value.size)
+
+ } else {
+
+ value.toUByteArray().asList().subList(4, value.size)
+
+ }
+
+ nextPackageDataCount = value.get2byteUIntAt(2)
+
+ resultTemperaturePackage.addAll(
+ temperatureDataArray.chunked(2).map {
+ it.toUByteArray().toTemperature()
+ }.toMutableList()
+ )
+
+ val expectedDataSize =
+ nextPackageDataCount.toInt() + resultTemperaturePackage.size
+
+ emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
+ emit(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize.toFloat())))
+
+ historyCharacteristic.write(DataByteArray.from(5))
+ value = historyCharacteristic.read().value
+
+ }
+
+ emit(
+ Result.success(
+ ProgressState.Finished(
+ resultTemperaturePackage.withIndex().map {
+ Ble.Thermometer.HistoryPoint(
+ date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
+ value = it.value
+ )
+ }
+ )
+ )
+
+ )
+
+ }
+
+ } catch (err: Throwable) {
+
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/UUIDs.kt b/common/src/main/java/llc/arma/common/UUIDs.kt
new file mode 100644
index 0000000..940ec51
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/UUIDs.kt
@@ -0,0 +1,78 @@
+package llc.arma.common
+
+import java.util.UUID
+import kotlin.math.PI
+import kotlin.math.atan
+
+val versionServiceUUID: UUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb")
+val firmwareVersionUUID: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
+
+val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
+
+val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb")
+val hostHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
+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")
+val saveEnabledWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+val passwordWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+val txWriteUUID: UUID = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb")
+val flashWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
+
+enum class AccelViewMode {
+ ACCELERATION, PEAK_ACCELERATION, RMS, VIBRATION, ANGLE, ROTATIONS;
+
+ companion object
+}
+
+enum class AccelScale(val k: Int) {
+
+ S_2(2_000), S_4(4_000), S_8(8_000), S_16(16_000);
+
+ companion object
+
+}
+
+enum class FftFrequency {
+ OFF, F_1, F_10, F_25, F_50, F_100, F_200, F_400, F_1620, F_1344;
+
+ companion object
+}
+
+enum class FftViewMode {
+ SPECTRE, X, Y, Z;
+
+ companion object
+}
+
+enum class FftAxis {
+ AUTO, X, Y, Z;
+
+ companion object
+}
+
+fun calculateAngle(
+ first: Float,
+ second: Float
+): Float {
+
+ var first = first
+
+ if(first == 0f && second == 0f){
+ first = 0.0001f
+ }
+
+ val result = when {
+ first > 0 -> atan(second / first)
+ first < 0 && second >= 0 -> atan(second / first) + PI.toFloat()
+ first < 0 && second < 0 -> atan(second/first) - PI.toFloat()
+ first == 0f && second > 0 -> PI.toFloat() / 2f
+ first == 0f && second < 0 -> -PI.toFloat() / 2f
+ else -> 0f
+ }
+
+ return result * 180f / Math.PI.toFloat()
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/domain/Ble.kt b/common/src/main/java/llc/arma/common/domain/Ble.kt
new file mode 100644
index 0000000..bca73f5
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/domain/Ble.kt
@@ -0,0 +1,185 @@
+package llc.arma.common.domain
+
+import llc.arma.common.AccelScale
+import llc.arma.common.AccelViewMode
+
+
+sealed class Ble(
+ var info: BleInfo,
+ var state: BleState,
+) {
+
+ class Accelerometer(
+ info: BleInfo,
+ state: BleState,
+ val accelerometerState: AccelerometerState
+ ): Ble(info, state) {
+
+ sealed class HistorySettings {
+
+ data class Enabled(
+ val scale: AccelScale,
+ val mode: AccelViewMode,
+ val detailed: Boolean
+ ) : HistorySettings()
+
+ data object Disabled : HistorySettings()
+
+ }
+
+ data class WriteRequest(
+ val tx: BleState.TX?,
+ val saveHistorySettings: HistorySettings?,
+ val historyInterval: Long?,
+ val readInterval: Long?,
+ )
+
+ sealed class HistoryPoint {
+
+ class Vibration (
+ val date: Long,
+ val value: Float
+ ) : HistoryPoint()
+
+ class Rotation (
+ val date: Long,
+ val value: Long
+ ) : HistoryPoint()
+
+ class Acceleration (
+ 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()
+
+ }
+
+ sealed class RealtimePoint {
+
+ data class Rotation(
+ val angle: Float,
+ val tmp: Float,
+ val turnovers: Int,
+ ) : RealtimePoint()
+
+ data class Common(
+ val x: Float,
+ val y: Float,
+ val z: Float,
+ ) : RealtimePoint()
+
+ data class Angle(
+ val x: Float,
+ val y: Float,
+ val z: Float,
+ ) : RealtimePoint()
+
+ data class Vibration(
+ val value: Float
+ ) : RealtimePoint()
+
+ }
+
+ class SpectrePoint (
+ val frequency: Long,
+ val value: Float
+ )
+
+ data class AccelerometerState(
+ val saveHistorySettings: HistorySettings,
+ val historyInterval: Long,
+ val readInterval: Long
+ )
+
+ }
+
+ class Beacon(
+ info: BleInfo,
+ state: BleState,
+ ) : Ble(info, state){
+
+ data class WriteRequest(
+ val tx: BleState.TX?
+ )
+
+ }
+
+ class Host(
+ info: BleInfo,
+ state: BleState,
+ val hostState: HostState
+ ) : Ble(info, state){
+
+ class HistoryPoint(
+ val date: Long,
+ val hit: Boolean,
+ val value: List
+ ){
+
+ data class Device(
+ val serial: String,
+ val rssi: Int?
+ )
+
+ }
+
+ data class HostState(
+ val historyInterval: Long,
+ val readInterval: Long
+ )
+
+ data class WriteRequest(
+ val tx: BleState.TX?,
+ val interval: Long?,
+ val readInterval: Long?,
+ )
+
+ }
+
+ class Thermometer(
+ info: BleInfo,
+ state: BleState,
+ val thermometerState: ThermometerState
+ ) : Ble(info, state) {
+
+ class HistoryPoint(
+ val date: Long,
+ val value: Float
+ )
+
+ data class ThermometerState(
+ val temperature: Float,
+ val saveHistory: Boolean,
+ val historyInterval: Long
+ )
+
+ data class WriteRequest(
+ val tx: BleState.TX?,
+ val saveHistory: Boolean?,
+ val historyInterval: Long?
+ )
+
+ }
+
+ data class BleState(
+ val tx: TX,
+ val version: Version
+ ){
+
+ enum class TX {
+ MINUS_40, MINUS_20, MINUS_16, MINUS_12, MINUS_8, MINUS_4, ZERO, PLUS_3, PLUS_4;
+
+ companion object
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/domain/BleInfo.kt b/common/src/main/java/llc/arma/common/domain/BleInfo.kt
new file mode 100644
index 0000000..fc30534
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/domain/BleInfo.kt
@@ -0,0 +1,26 @@
+package llc.arma.common.domain
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class BleInfo(
+ val name: String,
+ val serial: String,
+ val batteryLevel: Int,
+ val rssi: Int?,
+ val type: Type,
+ val scanTime: Long,
+ val tx: Int,
+ val tableStatus: HistoryTableStatus
+) : Parcelable {
+
+ enum class HistoryTableStatus {
+ DISABLED, EMPTY, NOT_EMPTY
+ }
+
+ enum class Type {
+ HOST, BEACON, THERMOMETER, ACCELEROMETER
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/domain/ProgressState.kt b/common/src/main/java/llc/arma/common/domain/ProgressState.kt
new file mode 100644
index 0000000..9bd0410
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/domain/ProgressState.kt
@@ -0,0 +1,13 @@
+package llc.arma.common.domain
+
+sealed class ProgressState {
+
+ data class Progress(
+ val value: Float
+ ) : ProgressState()
+
+ data class Finished(
+ val data: T
+ ) : ProgressState()
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/domain/Result.kt b/common/src/main/java/llc/arma/common/domain/Result.kt
new file mode 100644
index 0000000..84d9200
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/domain/Result.kt
@@ -0,0 +1,76 @@
+package llc.arma.common.domain
+
+class Result internal constructor(
+ val value: Any?
+) {
+
+ class Failure(
+ val error: E
+ )
+
+ companion object {
+
+ fun success(value: T): Result {
+ return Result(value)
+ }
+
+ fun failure(error: E): Result {
+ return Result(Failure(error))
+ }
+
+ }
+
+ val isSuccess: Boolean get() = value !is Failure<*>
+
+ val isFailure: Boolean get() = value is Failure<*>
+
+ inline fun fold(
+ onSuccess: (value: T) -> R,
+ onFailure: (error: E) -> R
+ ): R {
+ return when (value) {
+ is Failure<*> -> onFailure(value.error as E)
+ else -> onSuccess(value as T)
+ }
+ }
+
+ inline fun map(
+ mapper: (value: T) -> R,
+ ): Result {
+ return when (value) {
+ is Failure<*> -> failure(value.error as E)
+ else -> success(mapper.invoke(value as T))
+ }
+ }
+
+ inline fun onSuccess(
+ let: (result: T) -> Unit
+ ){
+ if(isSuccess){
+ let.invoke(value as T)
+ }
+ }
+
+ inline fun onFailure(
+ let: (error: E) -> Unit
+ ){
+ if(value is Failure<*>){
+ let.invoke(value.error as E)
+ }
+ }
+
+ fun getOrNull(): T? {
+ return when (value) {
+ is Failure<*> -> null
+ else -> value as T
+ }
+ }
+
+ fun getErrorOrNull(): E? {
+ return when (value) {
+ is Failure<*> -> value.error as E
+ else -> null
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/domain/Version.kt b/common/src/main/java/llc/arma/common/domain/Version.kt
new file mode 100644
index 0000000..ef6fb16
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/domain/Version.kt
@@ -0,0 +1,57 @@
+package llc.arma.common.domain
+
+data class Version(
+ val major: Int,
+ val minor: Int,
+ val patch: Int,
+ val release: Int
+): Comparable {
+
+ companion object {
+
+ fun fromString(
+ version: String
+ ): Version {
+
+ val splitArray = version.split('.', '-')
+
+ return Version(
+ major = splitArray[0].toInt(),
+ minor = splitArray[1].toInt(),
+ patch = splitArray[2].toInt(),
+ release = splitArray.last().toInt()
+ )
+
+ }
+
+ }
+
+ override fun compareTo(
+ other: Version
+ ): Int {
+
+ if(major != other.major){
+ return major - other.major
+ }
+
+ if(minor != other.minor){
+ return minor - other.minor
+ }
+
+ if(patch != other.patch){
+ return patch - other.patch
+ }
+
+ if(release != other.release){
+ return release - other.release
+ }
+
+ return 0
+
+ }
+
+ override fun toString(): String {
+ return "$major.$minor.$patch-$release"
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/extensions/BleEnumExtensions.kt b/common/src/main/java/llc/arma/common/extensions/BleEnumExtensions.kt
new file mode 100644
index 0000000..fd6074b
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/extensions/BleEnumExtensions.kt
@@ -0,0 +1,93 @@
+package llc.arma.common.extensions
+
+import llc.arma.common.AccelScale
+import llc.arma.common.AccelViewMode
+import llc.arma.common.FftAxis
+import llc.arma.common.FftFrequency
+import llc.arma.common.FftViewMode
+import llc.arma.common.domain.Ble
+
+fun Ble.BleState.TX.Companion.fromByte(byte: Byte): Ble.BleState.TX? {
+ return Ble.BleState.TX.entries.associateBy { it.sendData }[byte]
+}
+
+val Ble.BleState.TX.sendData: Byte
+ get() {
+ return when (this) {
+ Ble.BleState.TX.MINUS_40 -> -40
+ Ble.BleState.TX.MINUS_20 -> -20
+ Ble.BleState.TX.MINUS_16 -> -16
+ Ble.BleState.TX.MINUS_12 -> -12
+ Ble.BleState.TX.MINUS_8 -> -8
+ Ble.BleState.TX.MINUS_4 -> -4
+ Ble.BleState.TX.ZERO -> 0
+ Ble.BleState.TX.PLUS_3 -> 3
+ Ble.BleState.TX.PLUS_4 -> 4
+ }
+ }
+
+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
+ }
+ }
+
+fun AccelViewMode.Companion.fromByte(byte: Byte): AccelViewMode? {
+ return AccelViewMode.entries.associateBy { it.sendData }[byte]
+}
+
+val AccelViewMode.sendData: Byte
+ get() {
+ return when(this){
+ AccelViewMode.ACCELERATION -> 0
+ AccelViewMode.PEAK_ACCELERATION -> 1
+ AccelViewMode.RMS -> 2
+ AccelViewMode.VIBRATION -> 3
+ AccelViewMode.ANGLE -> 4
+ AccelViewMode.ROTATIONS -> 5
+ }
+ }
+
+fun AccelScale.Companion.fromByte(byte: Byte): AccelScale? {
+ return AccelScale.entries.associateBy { it.sendData }[byte]
+}
+
+val AccelScale.sendData: Byte
+ get() {
+ return when(this){
+ AccelScale.S_2 -> 0
+ AccelScale.S_4 -> 1
+ AccelScale.S_8 -> 2
+ AccelScale.S_16 -> 3
+ }
+ }
\ No newline at end of file
diff --git a/common/src/main/java/llc/arma/common/extensions/BleScanResultExtensions.kt b/common/src/main/java/llc/arma/common/extensions/BleScanResultExtensions.kt
new file mode 100644
index 0000000..069654c
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/extensions/BleScanResultExtensions.kt
@@ -0,0 +1,56 @@
+package llc.arma.common.extensions
+
+import llc.arma.common.domain.BleInfo
+import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult
+
+val BleScanResult.timerEnabled: BleInfo.HistoryTableStatus
+ get() {
+ return when(data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(2)){
+ 1.toByte() -> BleInfo.HistoryTableStatus.NOT_EMPTY
+ 2.toByte() -> BleInfo.HistoryTableStatus.EMPTY
+ else -> BleInfo.HistoryTableStatus.DISABLED
+ }
+ }
+
+val BleScanResult.info: BleInfo
+ get() {
+ this.device.name
+ return BleInfo(
+ name = this.device.name ?: "",
+ serial = device.address,
+ batteryLevel = batteryLevel ?: 0,
+ rssi = data?.rssi,
+ type = type,
+ scanTime = (data?.timestampNanos ?: 0) / 1_000_000,
+ tx = data?.scanRecord?.txPowerLevel ?: 0,
+ tableStatus = timerEnabled
+ )
+ }
+
+val BleScanResult.batteryLevel: Int?
+ get() {
+
+ val level = data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(1)
+ ?.toUByte()?.toInt()
+
+ if(data?.scanRecord?.deviceName?.contains("N00051023") == true){
+
+ println(level)
+ println(data?.scanRecord?.manufacturerSpecificData?.get(89))
+
+ }
+
+ return level
+
+ }
+
+val BleScanResult.type: BleInfo.Type
+ get() {
+ return when(data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(0)?.toUByte()?.toInt()){
+ 4 -> BleInfo.Type.HOST
+ 1 -> BleInfo.Type.BEACON
+ 2 -> BleInfo.Type.THERMOMETER
+ else -> BleInfo.Type.ACCELEROMETER
+ }
+ }
+
diff --git a/common/src/main/java/llc/arma/common/extensions/ByteArrayExtensions.kt b/common/src/main/java/llc/arma/common/extensions/ByteArrayExtensions.kt
new file mode 100644
index 0000000..360eea2
--- /dev/null
+++ b/common/src/main/java/llc/arma/common/extensions/ByteArrayExtensions.kt
@@ -0,0 +1,40 @@
+package llc.arma.common.extensions
+
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+fun ByteArray.get2byteShortAt(): Int {
+ val shorts = ShortArray(1)
+ ByteBuffer.wrap(this).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()[shorts]
+ return shorts[0].toInt()
+}
+
+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)
+
+fun ByteArray.get2byteUIntAt(idx: Int) =
+ ((this[idx + 1].toUInt() and 0xFFu) shl 8) or
+ (this[idx].toUInt() and 0xFFu)
+
+fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
+ (3 downTo 0).map {
+ (this shr (it * Byte.SIZE_BITS)).toByte()
+ }.toByteArray()
+
+@OptIn(ExperimentalUnsignedTypes::class)
+fun UByteArray.toTemperature(): Float {
+
+ val uShort = (this[0] + this[1] * 256u).toUShort()
+
+ val result = if (uShort > Short.MAX_VALUE.toUShort()) {
+ ((uShort.inv() + 1u).toFloat().unaryMinus()) / 100f
+ } else {
+ uShort.toFloat() / 100f
+ }
+
+ return result
+
+}
diff --git a/common/src/test/java/llc/arma/common/ExampleUnitTest.kt b/common/src/test/java/llc/arma/common/ExampleUnitTest.kt
new file mode 100644
index 0000000..2c8fa7c
--- /dev/null
+++ b/common/src/test/java/llc/arma/common/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package llc.arma.common
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 38f144c..26bb2b2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,6 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
+android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
@@ -23,4 +24,4 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=true
-org.gradle.unsafe.configuration-cache=true
\ No newline at end of file
+org.gradle.unsafe.configuration-cache=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..d273147
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,92 @@
+[versions]
+accompanistPermissions = "0.37.2"
+activityCompose = "1.10.1"
+agp = "8.9.1"
+client = "1.0.14"
+kotlinParcelizeRuntime = "2.1.20"
+vico-version = "1.7.1"
+composeBom = "2025.04.00"
+coreKtx = "1.16.0"
+coreSplashscreen = "1.0.1"
+datastorePreferences = "1.1.4"
+espressoCore = "3.6.1"
+hilt-version = "2.56.2"
+hiltCompiler = "1.2.0"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+kotlin = "2.1.20"
+kotlinxSerializationJson = "1.8.0"
+ksp-version = "2.1.20-1.0.32"
+lifecycleRuntimeKtx = "2.8.7"
+material = "1.7.8"
+material3 = "1.4.0-alpha15"
+navigationCompose = "2.9.0-beta01"
+room-version = "2.7.0"
+scanner = "1.0.14"
+ui = "1.7.8"
+uiTestJunit4 = "1.7.8"
+uiTestManifest = "1.7.8"
+uiTooling = "1.7.8"
+uiToolingPreview = "1.7.8"
+appcompat = "1.7.0"
+materialVersion = "1.12.0"
+kotlin-serialization = "2.1.20"
+workRuntimeKtx = "2.10.1"
+
+[libraries]
+accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
+androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
+androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltCompiler" }
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltCompiler" }
+androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hiltCompiler" }
+androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
+androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
+androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
+androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material" }
+androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room-version" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room-version" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room-version" }
+androidx-ui = { module = "androidx.compose.ui:ui" }
+androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
+androidx-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
+androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
+androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
+client = { module = "no.nordicsemi.android.kotlin.ble:client", version.ref = "client" }
+compose = { module = "com.patrykandpatrick.vico:compose", version.ref = "vico-version" }
+compose-m3 = { module = "com.patrykandpatrick.vico:compose-m3", version.ref = "vico-version" }
+core = { module = "com.patrykandpatrick.vico:core", version.ref = "vico-version" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt-version" }
+hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt-version" }
+junit = { module = "junit:junit", version.ref = "junit" }
+kotlin-parcelize-runtime = { module = "org.jetbrains.kotlin:kotlin-parcelize-runtime", version.ref = "kotlinParcelizeRuntime" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
+material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
+scanner = { module = "no.nordicsemi.android.kotlin.ble:scanner", version.ref = "scanner" }
+ui = { module = "androidx.compose.ui:ui", version.ref = "ui" }
+ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "uiTestJunit4" }
+ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" }
+ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" }
+ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreview" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "materialVersion" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt-version" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-version" }
+room = { id = 'androidx.room', version.ref = "room-version" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-serialization"}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df97d72..b00c096 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
+#Fri Apr 18 13:44:40 KRAT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle b/settings.gradle
index a452f5f..373dcf4 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,7 +10,11 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
+ maven { url 'https://jitpack.io' }
}
}
rootProject.name = "Ble"
include ':app'
+include ':tester'
+include ':common'
+include ':vgate'
diff --git a/tester/.gitignore b/tester/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/tester/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/tester/build.gradle.kts b/tester/build.gradle.kts
new file mode 100644
index 0000000..9bf49fd
--- /dev/null
+++ b/tester/build.gradle.kts
@@ -0,0 +1,105 @@
+import com.android.build.gradle.internal.api.BaseVariantOutputImpl
+
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.room)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+
+ namespace = "llc.arma.tester"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "llc.arma.tester"
+ minSdk = 24
+ targetSdk = 35
+ versionCode = 1
+ versionName = "0.0.1"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ applicationVariants.all {
+ outputs.all {
+ (this as BaseVariantOutputImpl).outputFileName = "Arma BLE tester v${defaultConfig.versionName}.apk"
+ }
+ }
+
+ room {
+ schemaDirectory("$projectDir/schemas")
+ }
+
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ implementation(libs.accompanist.permissions)
+
+ implementation(libs.androidx.material.icons.extended)
+
+ implementation(libs.androidx.navigation.compose)
+
+ implementation(libs.androidx.hilt.navigation.compose)
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.android.compiler)
+ ksp(libs.androidx.hilt.compiler)
+
+ implementation(libs.androidx.room.runtime)
+ ksp(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlin.parcelize.runtime)
+
+ implementation(libs.core)
+ implementation(libs.compose)
+ implementation(libs.compose.m3)
+
+ implementation(project(":common"))
+
+}
\ No newline at end of file
diff --git a/tester/proguard-rules.pro b/tester/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/tester/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/tester/src/androidTest/java/llc/arma/tester/ExampleInstrumentedTest.kt b/tester/src/androidTest/java/llc/arma/tester/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..d0dafa6
--- /dev/null
+++ b/tester/src/androidTest/java/llc/arma/tester/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package llc.arma.tester
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("llc.arma.tester", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/tester/src/main/AndroidManifest.xml b/tester/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ebb788a
--- /dev/null
+++ b/tester/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/App.kt b/tester/src/main/java/llc/arma/tester/app/App.kt
new file mode 100644
index 0000000..3d0946a
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/App.kt
@@ -0,0 +1,27 @@
+package llc.arma.tester.app
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+import java.io.File
+import java.io.IOException
+
+
+@HiltAndroidApp
+class App : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val logDirectory = File(filesDir.absolutePath + "/logs")
+ val logFile = File(logDirectory, "logcat_" + System.currentTimeMillis() + ".txt")
+
+ if (!logDirectory.exists()) {
+ logDirectory.mkdir()
+ }
+
+ var process = Runtime.getRuntime().exec("logcat -c")
+ process = Runtime.getRuntime().exec("logcat -f $logFile")
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/BaseViewModel.kt b/tester/src/main/java/llc/arma/tester/app/BaseViewModel.kt
new file mode 100644
index 0000000..06f8302
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/BaseViewModel.kt
@@ -0,0 +1,63 @@
+package llc.arma.tester.app
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+abstract class BaseViewModel<
+ UiState : ViewState,
+ Event : ViewEvent,
+ Effect : ViewSideEffect> : ViewModel() {
+
+ private val initialState: UiState by lazy { setInitialState() }
+ abstract fun setInitialState(): UiState
+
+ private val _viewState: MutableState = mutableStateOf(initialState)
+ val viewState: State = _viewState
+
+ private val _event: MutableSharedFlow = MutableSharedFlow()
+
+ private val _effect: Channel = Channel()
+ val effect = _effect.receiveAsFlow()
+
+ init {
+ subscribeToEvents()
+ }
+
+ fun setEvent(event: Event) {
+ viewModelScope.launch { _event.emit(event) }
+ }
+
+ fun setState(reducer: UiState.() -> UiState) {
+ val newState = viewState.value.reducer()
+ _viewState.value = newState
+ }
+
+ private fun subscribeToEvents() {
+ viewModelScope.launch {
+ _event.collect {
+ handleEvents(it)
+ }
+ }
+ }
+
+ protected abstract fun handleEvents(event: Event)
+
+ protected fun setEffect(builder: () -> Effect) {
+ val effectValue = builder()
+ viewModelScope.launch { _effect.send(effectValue) }
+ }
+
+}
+
+interface ViewState
+
+interface ViewEvent
+
+interface ViewSideEffect
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/MainActivity.kt b/tester/src/main/java/llc/arma/tester/app/MainActivity.kt
new file mode 100644
index 0000000..dfba172
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/MainActivity.kt
@@ -0,0 +1,67 @@
+package llc.arma.tester.app
+
+import android.Manifest
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.runtime.LaunchedEffect
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.rememberMultiplePermissionsState
+import dagger.hilt.android.AndroidEntryPoint
+import llc.arma.tester.app.ui.screens.main.MainScreen
+import llc.arma.tester.app.ui.screens.tests.gate.test.KeepScreenOn
+import llc.arma.tester.app.ui.theme.BleTheme
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+
+ @OptIn(ExperimentalPermissionsApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ enableEdgeToEdge()
+
+ setContent {
+
+ KeepScreenOn()
+
+ BleTheme {
+
+ val multiplePermissionsState =
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ rememberMultiplePermissionsState(
+ listOf(
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT
+ )
+ )
+ } else {
+ rememberMultiplePermissionsState(
+ listOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ )
+ )
+ }
+
+ if (multiplePermissionsState.allPermissionsGranted) {
+
+ MainScreen()
+
+ } else {
+
+ LaunchedEffect(multiplePermissionsState) {
+ multiplePermissionsState.launchMultiplePermissionRequest()
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+}
diff --git a/tester/src/main/java/llc/arma/tester/app/di/DatabaseModule.kt b/tester/src/main/java/llc/arma/tester/app/di/DatabaseModule.kt
new file mode 100644
index 0000000..a596f57
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/di/DatabaseModule.kt
@@ -0,0 +1,49 @@
+package llc.arma.tester.app.di
+
+import android.app.Application
+import androidx.room.Room
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import llc.arma.tester.data.database.AppDatabase
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class DatabaseModule {
+
+ @Singleton
+ @Provides
+ fun provideDatabase(
+ app: Application
+ ): AppDatabase {
+
+ return Room.databaseBuilder(
+ context = app,
+ klass = AppDatabase::class.java,
+ name = "app_database"
+ ).build()
+
+ }
+
+ @Singleton
+ @Provides
+ fun provideTestDao(
+ database: AppDatabase
+ ) = database.getGateTestDao()
+
+ @Singleton
+ @Provides
+ fun provideSelectedBleDao(
+ database: AppDatabase
+ ) = database.getSelectedBleDao()
+
+ @Singleton
+ @Provides
+ fun provideReadResultDao(
+ database: AppDatabase
+ ) = database.getReadResultDao()
+
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/di/RepositoryModule.kt b/tester/src/main/java/llc/arma/tester/app/di/RepositoryModule.kt
new file mode 100644
index 0000000..9e65f8e
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/di/RepositoryModule.kt
@@ -0,0 +1,40 @@
+package llc.arma.tester.app.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import llc.arma.tester.data.repository.GateHistoryPointRepositoryImpl
+import llc.arma.tester.data.repository.GateTestRepositoryImpl
+import llc.arma.tester.data.repository.ReadResultRepositoryImpl
+import llc.arma.tester.data.repository.SelectedBleRepositoryImpl
+import llc.arma.tester.domain.repository.GateHistoryPointRepository
+import llc.arma.tester.domain.repository.GateTestRepository
+import llc.arma.tester.domain.repository.ReadResultRepository
+import llc.arma.tester.domain.repository.SelectedBleRepository
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface RepositoryModule {
+
+ @Binds
+ fun bindGateTestRepository(
+ gateTestRepository: GateTestRepositoryImpl
+ ): GateTestRepository
+
+ @Binds
+ fun bindSelectedBleRepository(
+ selectedBleRepository: SelectedBleRepositoryImpl
+ ): SelectedBleRepository
+
+ @Binds
+ fun bindReadResultRepository(
+ readResultRepository: ReadResultRepositoryImpl
+ ): ReadResultRepository
+
+ @Binds
+ fun bindGateHistoryPointRepository(
+ gateHistoryPointRepository: GateHistoryPointRepositoryImpl
+ ): GateHistoryPointRepository
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/mapper/GateReadResultsMapper.kt b/tester/src/main/java/llc/arma/tester/app/ui/mapper/GateReadResultsMapper.kt
new file mode 100644
index 0000000..c6bf85f
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/mapper/GateReadResultsMapper.kt
@@ -0,0 +1,39 @@
+package llc.arma.tester.app.ui.mapper
+
+import llc.arma.tester.app.ui.model.GateReadResultsWithPoints
+import llc.arma.tester.data.mapper.Mapper
+import llc.arma.tester.data.mapper.SuspendMapper
+import llc.arma.tester.domain.model.GateReadResult
+import llc.arma.tester.domain.usecase.GetGateHistoryPointsByResultId
+import javax.inject.Inject
+
+class GateReadResultsMapper @Inject constructor(
+ private val getGateHistoryPointsByResultId: GetGateHistoryPointsByResultId
+) : SuspendMapper {
+
+ override suspend fun map(
+ input: GateReadResult
+ ): GateReadResultsWithPoints {
+
+ return when(input){
+ is GateReadResult.Error -> {
+ GateReadResultsWithPoints.Error(
+ id = input.id,
+ bleId = input.bleId,
+ date = input.date
+ )
+ }
+ is GateReadResult.Success -> {
+ GateReadResultsWithPoints.Success(
+ id = input.id,
+ bleId = input.bleId,
+ date = input.date,
+ gateRssi = input.gateRssi,
+ points = getGateHistoryPointsByResultId.invoke(input.id)
+ )
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/model/GateReadResultsWithPoints.kt b/tester/src/main/java/llc/arma/tester/app/ui/model/GateReadResultsWithPoints.kt
new file mode 100644
index 0000000..31cab9a
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/model/GateReadResultsWithPoints.kt
@@ -0,0 +1,26 @@
+package llc.arma.tester.app.ui.model
+
+import llc.arma.tester.domain.model.GateHistoryPoint
+import llc.arma.tester.domain.model.GateReadResult
+
+sealed class GateReadResultsWithPoints(
+ var id: Long,
+ var bleId: Long,
+ var date: Long
+) {
+
+ class Error(
+ id: Long,
+ bleId: Long,
+ date: Long,
+ ) : GateReadResultsWithPoints(id, bleId, date)
+
+ class Success(
+ id: Long,
+ bleId: Long,
+ date: Long,
+ val gateRssi: Long,
+ val points: List
+ ) : GateReadResultsWithPoints(id, bleId, date)
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeContract.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeContract.kt
new file mode 100644
index 0000000..ef0982e
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeContract.kt
@@ -0,0 +1,38 @@
+package llc.arma.tester.app.ui.screens.home
+
+import llc.arma.tester.app.ViewEvent
+import llc.arma.tester.app.ViewSideEffect
+import llc.arma.tester.app.ViewState
+import llc.arma.tester.data.entity.GateTestEntity
+
+class HomeContract {
+
+ sealed class Event : ViewEvent {
+
+ data class OnTestClick(
+ val testId: Long
+ ) : Event()
+
+ data object OnNewTestClick : Event()
+
+ }
+
+ data class State(
+ val tests: List
+ ) : ViewState
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data class NavigateToTest(
+ val testId: Long
+ ) : Navigation()
+
+ data object NavigateToNewTest : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeScreen.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeScreen.kt
new file mode 100644
index 0000000..bfced11
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeScreen.kt
@@ -0,0 +1,144 @@
+package llc.arma.tester.app.ui.screens.home
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.PlayArrow
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import llc.arma.tester.data.entity.GateTestEntity
+import java.text.SimpleDateFormat
+import java.util.Date
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HomeScreen(
+ onNavigationEvent: (HomeContract.Effect.Navigation) -> Unit
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+
+ viewModel.effect.collect {
+
+ when(it){
+ is HomeContract.Effect.Navigation -> onNavigationEvent(it)
+ }
+
+ }
+
+ }
+
+ val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ scrollBehavior = scrollBehavior,
+ title = {
+ Text(text = "BLE tester")
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest
+ )
+ )
+ },
+ floatingActionButton = {
+ ExtendedFloatingActionButton(
+ text = {
+ Text(text = "Новый тест")
+ },
+ icon = {
+ Icon(
+ imageVector = Icons.Rounded.PlayArrow,
+ contentDescription = null
+ )
+ },
+ onClick = {
+ onNavigationEvent(HomeContract.Effect.Navigation.NavigateToNewTest)
+ }
+ )
+ },
+ modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
+ ) {
+
+ LazyColumn(
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(it)
+ ) {
+
+ items(items = state.tests){
+
+ TestItem(it){
+ viewModel.setEvent(HomeContract.Event.OnTestClick(it.id))
+ }
+
+ }
+
+ }
+
+ }
+
+}
+
+@Composable
+fun TestItem(
+ test: GateTestEntity,
+ onClick: () -> Unit
+){
+ val duration = ((test.endMillis ?: 0) - test.startMillis).toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
+ "${hours}ч. ${minutes}м. ${seconds}с."
+ }
+
+ Surface(
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ onClick = onClick,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+
+ Text(
+ text = "Тест шлюза ${SimpleDateFormat.getDateTimeInstance(3, 3).format(Date(test.startMillis))}"
+ )
+ Text(
+ color = LocalContentColor.current.copy(alpha = 0.5f),
+ style = MaterialTheme.typography.bodyMedium,
+ text = "Длительность: $duration"
+ )
+
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeViewModel.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeViewModel.kt
new file mode 100644
index 0000000..17b6e69
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/home/HomeViewModel.kt
@@ -0,0 +1,44 @@
+package llc.arma.tester.app.ui.screens.home
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import llc.arma.tester.app.BaseViewModel
+import llc.arma.tester.data.repository.GateTestRepositoryImpl
+import javax.inject.Inject
+
+@HiltViewModel
+class HomeViewModel @Inject constructor(
+ gateTestRepositoryImpl: GateTestRepositoryImpl
+) : BaseViewModel() {
+
+ init {
+
+ gateTestRepositoryImpl.getAllFlow().onEach {
+ setState { copy(tests = it) }
+ }.launchIn(viewModelScope)
+
+ }
+
+ override fun setInitialState() = HomeContract.State(emptyList())
+
+ override fun handleEvents(event: HomeContract.Event) {
+ when(event){
+ is HomeContract.Event.OnTestClick -> reduce(viewState.value, event)
+ HomeContract.Event.OnNewTestClick -> TODO()
+ }
+ }
+
+ private fun reduce(
+ state: HomeContract.State,
+ event: HomeContract.Event.OnTestClick
+ ) {
+
+ setEffect {
+ HomeContract.Effect.Navigation.NavigateToTest(event.testId)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainContract.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainContract.kt
new file mode 100644
index 0000000..1c0d5d7
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainContract.kt
@@ -0,0 +1,23 @@
+package llc.arma.tester.app.ui.screens.main
+
+import llc.arma.tester.app.ViewEvent
+import llc.arma.tester.app.ViewSideEffect
+import llc.arma.tester.app.ViewState
+
+class MainContract {
+
+ sealed class Event : ViewEvent {}
+
+ sealed class State : ViewState {
+
+ data object Loading : State()
+
+ data object Display : State()
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainScreen.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainScreen.kt
new file mode 100644
index 0000000..1f29829
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainScreen.kt
@@ -0,0 +1,173 @@
+package llc.arma.tester.app.ui.screens.main
+
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import kotlinx.serialization.Serializable
+import llc.arma.common.domain.BleInfo
+import llc.arma.tester.app.ui.screens.home.HomeContract
+import llc.arma.tester.app.ui.screens.home.HomeScreen
+import llc.arma.tester.app.ui.screens.selector.BleSelectorContract
+import llc.arma.tester.app.ui.screens.selector.BleSelectorScreen
+import llc.arma.tester.app.ui.screens.tests.gate.ble.BleReadResultsContract
+import llc.arma.tester.app.ui.screens.tests.gate.ble.BleReadResultsScreen
+import llc.arma.tester.app.ui.screens.tests.gate.master.GateTestMasterContract
+import llc.arma.tester.app.ui.screens.tests.gate.master.GateTestMasterScreen
+import llc.arma.tester.app.ui.screens.tests.gate.rssi.CrossHostBleContract
+import llc.arma.tester.app.ui.screens.tests.gate.rssi.CrossHostBleScreen
+import llc.arma.tester.app.ui.screens.tests.gate.test.GateTestContract
+import llc.arma.tester.app.ui.screens.tests.gate.test.GateTestScreen
+
+@Serializable
+data object HomeScreenRoute
+
+@Serializable
+data class GateTestScreenRoute(
+ val id: Long
+)
+
+@Serializable
+data class BleSelectorScreenRoute(
+ val type: BleInfo.Type?
+)
+
+@Serializable
+data class ReadResultsScreenRoute(
+ val selectedBleId: Long
+)
+
+@Serializable
+data class CrossHostBleRoute(
+ val testId: Long
+)
+
+@Serializable object GateTestMasterScreenRoute
+
+@Composable
+fun MainScreen() {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ if(state is MainContract.State.Display) {
+
+ val controller = rememberNavController()
+
+ NavHost(
+ navController = controller,
+ startDestination = HomeScreenRoute,
+ modifier = Modifier.navigationBarsPadding()
+ ) {
+
+ composable {
+
+ val selectResult = controller.currentBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow?>("selectBleResult", null)
+ ?.collectAsState()?.value
+
+ GateTestMasterScreen(
+ selectedBle = selectResult,
+ onNavigationEvent = {
+ when (it) {
+ GateTestMasterContract.Effect.Navigation.NavigateToBleSelector -> {
+ controller.navigate(BleSelectorScreenRoute(type = BleInfo.Type.HOST))
+ }
+
+ GateTestMasterContract.Effect.Navigation.NavigateUp -> controller.popBackStack()
+ is GateTestMasterContract.Effect.Navigation.NavigateToGateTest -> {
+ controller.navigate(GateTestScreenRoute(id = it.id))
+ }
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ BleReadResultsScreen(
+ onNavigationEvent = {
+ when(it){
+ BleReadResultsContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ HomeScreen(
+ onNavigationEvent = {
+ when (it) {
+ is HomeContract.Effect.Navigation.NavigateToTest ->
+ controller.navigate(GateTestScreenRoute(id = it.testId))
+
+ is HomeContract.Effect.Navigation.NavigateToNewTest ->
+ controller.navigate(GateTestMasterScreenRoute)
+ }
+
+ }
+ )
+
+ }
+
+ composable {
+
+ BleSelectorScreen(
+ onNavigationEvent = {
+ when (it) {
+ BleSelectorContract.Effect.Navigation.OnNavigateUp -> controller.navigateUp()
+ is BleSelectorContract.Effect.Navigation.OnNavigateUpWithResult -> {
+ controller.previousBackStackEntry?.savedStateHandle?.set(
+ "selectBleResult",
+ it.selected
+ )
+ controller.popBackStack()
+ }
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ GateTestScreen(
+ onNavigationEvent = {
+ when(it){
+ is GateTestContract.Effect.Navigation.NavigateToResults ->
+ controller.navigate(ReadResultsScreenRoute(it.selectedBleId))
+
+ is GateTestContract.Effect.Navigation.NavigateToCrossHost ->
+ controller.navigate(CrossHostBleRoute(it.testId))
+ }
+ }
+ )
+
+ }
+
+ composable {
+
+ CrossHostBleScreen(
+ onNavigationEvent = {
+ when(it){
+ CrossHostBleContract.Effect.Navigation.NavigateUp ->
+ controller.popBackStack()
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainViewModel.kt b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainViewModel.kt
new file mode 100644
index 0000000..079a20b
--- /dev/null
+++ b/tester/src/main/java/llc/arma/tester/app/ui/screens/main/MainViewModel.kt
@@ -0,0 +1,33 @@
+package llc.arma.tester.app.ui.screens.main
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import llc.arma.tester.app.BaseViewModel
+import llc.arma.tester.domain.usecase.FinishStartedTests
+import javax.inject.Inject
+
+@HiltViewModel
+class MainViewModel @Inject constructor(
+ private val finishStartedTests: FinishStartedTests
+) : BaseViewModel