Compare commits

..

10 Commits

Author SHA1 Message Date
Vineyro 02138f3f2f email sender 2025-06-17 03:52:31 +07:00
Vineyro 39297abc6c total refactor 2025-06-16 12:24:55 +07:00
Vineyro 435a4db2fb total refactor 2025-06-05 14:50:14 +07:00
Vineyro 20c8842f95 ble api update 2025-04-18 11:54:28 +07:00
Vineyro 0b8599dafa ble api update 2024-11-16 13:45:03 +07:00
Vineyro 2d8ec33c6c ble api update 2024-11-15 17:01:42 +07:00
Vineyro a2cc797320 ble api update 2024-11-02 16:31:58 +07:00
Vineyro ab27faa832 filter saving 2024-10-18 17:00:51 +07:00
Vineyro 666757922d filter saving 2024-09-27 14:59:49 +07:00
Vineyro 5293604ee4 xlsx export 2024-08-06 17:18:20 +07:00
431 changed files with 29911 additions and 15676 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -4,6 +4,36 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-06-11T08:44:02.119013Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=BV5900DNS00004122" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="tester">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-27T09:31:03.968211200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=S96PROEEA0000067711" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="vgate">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-23T08:00:06.927511100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=S96PROEEA0000067711" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@ -4,15 +4,18 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/common" />
<option value="$PROJECT_DIR$/tester" />
<option value="$PROJECT_DIR$/vgate" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -1,20 +1,56 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
@ -25,8 +61,13 @@
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JsCompilerArguments">
<option name="moduleKind" value="plain" />
</component>
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
<option name="version" value="2.1.20" />
</component>
</project>

View File

@ -1,6 +1,12 @@
<project version="4">
<component name="EntryPointsManager">
<list size="2">
<item index="0" class="java.lang.String" itemvalue="dagger.Binds" />
<item index="1" class="java.lang.String" itemvalue="dagger.Module" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -1,112 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
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 35
versionName "1.4.4"
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 files('libs/poishadow-all.jar')
}

133
app/build.gradle.kts Normal file
View File

@ -0,0 +1,133 @@
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("org.slf4j:slf4j-simple:2.1.0-alpha1")
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"))
}

191
app/proguard-rules.pro vendored
View File

@ -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
@ -18,4 +18,191 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-dontwarn org.apache.**
-dontwarn org.openxmlformats.schemas.**
-dontwarn org.etsi.**
-dontwarn org.w3.**
-dontwarn com.microsoft.schemas.**
-dontwarn com.graphbuilder.**
-dontnote org.apache.**
-dontnote org.openxmlformats.schemas.**
-dontnote org.etsi.**
-dontnote org.w3.**
-dontnote com.microsoft.schemas.**
-dontnote com.graphbuilder.**
-keeppackagenames org.apache.poi.ss.formula.function
-keep class org.apache.**
-keep class com.fasterxml.aalto.stax.InputFactoryImpl
-keep class com.fasterxml.aalto.stax.OutputFactoryImpl
-keep class com.fasterxml.aalto.stax.EventFactoryImpl
-keep class schemaorg_apache_xmlbeans.system.sF1327CCA741569E70F9CA8C9AF9B44B2.TypeSystemHolder { public final static *** typeSystem; }
-keep class org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem { public static *** get(...); public static *** getNoType(...); }
-keep class org.apache.xmlbeans.impl.schema.PathResourceLoader { public <init>(...); }
-keep class org.apache.xmlbeans.impl.schema.SchemaTypeSystemCompiler { public static *** compile(...); }
-keep class org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl { public <init>(...); public static *** get(...); public static *** getNoType(...); }
-keep class org.apache.xmlbeans.impl.schema.SchemaTypeLoaderImpl { public static *** getContextTypeLoader(...); public static *** build(...); }
-keep class org.apache.xmlbeans.impl.store.Locale { public static *** streamToNode(...); public static *** nodeTo*(...); }
-keep class org.apache.xmlbeans.impl.store.Path { public static *** compilePath(...); }
-keep class org.apache.xmlbeans.impl.store.Query { public static *** compileQuery(...); }
-keep class com.google.errorprone.annotations.MustBeClosed { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CommentsDocument { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTAuthors { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBooleanProperty { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBookView { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBookViews { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBorder { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBorders { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTBorderPr { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCell { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCellAlignment { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCellFormula { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCellStyleXfs { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCellXfs { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTColor { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTComment { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTComments { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCommentList { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTDrawing { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFill { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFills { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFonts { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFontName { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFontScheme { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFontSize { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTIntProperty { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTLegacyDrawing { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTNumFmts { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPatternFill { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPageMargins { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTPane { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRow { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSelection { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheet { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheetData { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheetDimension { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheetFormatPr { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheetView { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheetViews { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSheets { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTSst { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTStylesheet { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRst { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbook { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbookPr { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorksheet { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.CTXf { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.SstDocument { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.StyleSheetDocument { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellType$Enum { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.STCellFormulaType$Enum { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.STXstring { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CommentsDocumentImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTAuthorsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBooleanPropertyImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBookViewImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBookViewsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBorderImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBordersImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTBorderPrImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellAlignmentImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellFormulaImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellStyleXfsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellXfsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTColorImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTColImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTColsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCommentImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCommentsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCommentListImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTDrawingImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFillImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFillsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFontImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFontsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFontNameImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFontSchemeImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTFontSizeImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTIntPropertyImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTLegacyDrawingImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTNumFmtsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTPatternFillImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTPageMarginsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTPaneImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRowImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSelectionImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetDataImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetDimensionImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetFormatPrImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetViewImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetViewsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSheetsImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTSstImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTStylesheetImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRstImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTWorkbookImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTWorkbookPrImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTWorksheetImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTXfImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.SstDocumentImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.StyleSheetDocumentImpl { *; }
-keep class org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STXstringImpl { *; }
-keep class org.openxmlformats.schemas.officeDocument.x2006.customProperties.impl.CTPropertiesImpl { *; }
-keep class org.openxmlformats.schemas.officeDocument.x2006.customProperties.impl.PropertiesDocumentImpl { *; }
-keep class org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.impl.CTPropertiesImpl { *; }
-keep class org.openxmlformats.schemas.officeDocument.x2006.extendedProperties.impl.PropertiesDocumentImpl { *; }
-keep class org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.impl.CTDrawingImpl { *; }
-keep class org.openxmlformats.schemas.drawingml.x2006.spreadsheetDrawing.impl.CTMarkerImpl { *; }
-keep class com.microsoft.schemas.office.office.impl.CTIdMapImpl { *; }
-keep class com.microsoft.schemas.office.office.impl.CTShapeLayoutImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTShadowImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTFillImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTPathImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTShapeImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTShapetypeImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTStrokeImpl { *; }
-keep class com.microsoft.schemas.vml.impl.CTTextboxImpl { *; }
-keep class com.microsoft.schemas.office.excel.impl.CTClientDataImpl { *; }
-keep class com.microsoft.schemas.office.excel.impl.STTrueFalseBlankImpl { *; }
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@ -18,7 +18,6 @@
tools:targetApi="s" />
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.bluetooth_le"
android:required="true"/>
@ -56,7 +55,7 @@
<activity
android:name=".app.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.App.Starting">
android:theme="@style/Theme.Ble">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -8,11 +8,13 @@ import llc.arma.ble.data.repository.BleNameRepositoryImpl
import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.data.repository.EmailRepositoryImpl
import llc.arma.ble.data.repository.RotationsRepositoryImpl
import llc.arma.ble.data.repository.SettingsRepositoryImpl
import llc.arma.ble.data.repository.XlsxRepositoryImpl
import llc.arma.ble.domain.repository.BleNameRepository
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.repository.EmailRepository
import llc.arma.ble.domain.repository.RotationsRepository
import llc.arma.ble.domain.repository.SettingsRepository
import llc.arma.ble.domain.repository.XlsxRepository
@Module
@ -34,4 +36,7 @@ interface RepositoryBinding {
@Binds
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository
@Binds
fun bindSettingsRepository(repository: SettingsRepositoryImpl): SettingsRepository
}

View File

@ -7,68 +7,51 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Bundle
import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
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
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BottomState
import llc.arma.ble.app.ui.common.LocalBottomDialogState
import llc.arma.ble.app.ui.screen.main.MainScreen
import llc.arma.ble.app.ui.theme.BleTheme
import org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
init {
System.setProperty(DEFAULT_LOG_LEVEL_KEY, "Debug")
}
@SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
installSplashScreen()
@ -76,164 +59,88 @@ class MainActivity : ComponentActivity() {
BleTheme {
val modalState =
rememberModalBottomSheetState(
skipHalfExpanded = true,
initialValue = ModalBottomSheetValue.Hidden
)
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Surface(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
color = MaterialTheme.colorScheme.background
) {
var sheetContent by remember() {
mutableStateOf<@Composable () -> Unit>({})
}
val multiplePermissionsState =
if(modalState.currentValue == ModalBottomSheetValue.Hidden){
sheetContent = {}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
CompositionLocalProvider(
LocalBottomDialogState provides BottomState(
sheetState = modalState,
setContent = {
sheetContent = it
}
)
) {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
)
ModalBottomSheetLayout(
sheetShape = RoundedCornerShape(
topStart = 25.dp,
topEnd = 25.dp
),
sheetElevation = 0.dp,
sheetState = modalState,
sheetContent = {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.statusBarsPadding().navigationBarsPadding()
) {
Spacer(modifier = Modifier.height(14.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(
width = 54.dp,
height = 5.dp
)
) {}
Spacer(modifier = Modifier.height(12.dp))
sheetContent()
}
}
}
BackHandler(modalState.isVisible) {
scope.launch { modalState.hide() }
}
},
content = {
Surface(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
color = MaterialTheme.colorScheme.background
) {
val multiplePermissionsState =
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.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.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)
}
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LaunchedEffect(lifecycleState){
bleEnabled = mBluetoothAdapter.isEnabled
}
if (multiplePermissionsState.allPermissionsGranted) {
if(bleEnabled) {
MainScreen()
} else {
val context = LocalContext.current
LaunchedEffect(mBluetoothAdapter.isEnabled){
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
(context as Activity).startActivityForResult(intent, 1)
}
}
)
} else {
LaunchedEffect(multiplePermissionsState) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
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)
}
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LaunchedEffect(lifecycleState) {
bleEnabled = mBluetoothAdapter.isEnabled
}
if (multiplePermissionsState.allPermissionsGranted) {
if (bleEnabled) {
MainScreen()
} else {
val context = LocalContext.current
LaunchedEffect(mBluetoothAdapter.isEnabled) {
val intent =
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
(context as Activity).startActivityForResult(
intent,
1
)
}
}
} else {
LaunchedEffect(multiplePermissionsState) {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
)
}
}
}
@ -242,23 +149,4 @@ class MainActivity : ComponentActivity() {
}
}
@Composable
fun Camera(){
AndroidView(
factory = {
val cameraManager = it.getSystemService(CameraManager::class.java)
cameraManager.cameraIdList.forEach {
println("$it is front: ${cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT}")
}
return@AndroidView SurfaceView(it)
}
)
}

View File

@ -1,50 +0,0 @@
package llc.arma.ble.app.ui.common
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
val LocalBottomDialogState = compositionLocalOf<BottomState?> { null }
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun rememberBottomDialogState(): BottomDialogState {
val state = LocalBottomDialogState.current
return remember {
BottomDialogState(
sheetState = state?.sheetState,
setContent = state?.setContent ?: { }
)
}
}
class BottomState @OptIn(ExperimentalMaterialApi::class) constructor(
val sheetState: ModalBottomSheetState?,
val setContent: (@Composable () -> Unit) -> Unit,
)
class BottomDialogState @OptIn(ExperimentalMaterialApi::class) constructor(
val sheetState: ModalBottomSheetState?,
val setContent: (@Composable () -> Unit) -> Unit,
) {
@OptIn(ExperimentalMaterialApi::class)
suspend fun show(
content: @Composable () -> Unit
){
setContent(content)
//if(sheetState?.currentValue != ModalBottomSheetValue.Expanded)
sheetState?.show()
}
@OptIn(ExperimentalMaterialApi::class)
suspend fun hide(){
sheetState?.hide()
setContent { }
}
}

View File

@ -0,0 +1,110 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.layout.Box
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.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PrimaryButton(
modifier: Modifier = Modifier,
label: String,
onClick: () -> Unit
) {
Surface(
modifier = modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = onClick
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = label
)
}
}
}
@Composable
fun SmallPrimaryButton(
modifier: Modifier = Modifier,
label: String,
onClick: () -> Unit
) {
Surface(
modifier = modifier
.padding(8.dp)
.height(48.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = onClick
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = label
)
}
}
}
@Composable
fun SecondaryButton(
modifier: Modifier = Modifier,
label: String,
onClick: () -> Unit
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = onClick,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = label
)
}
}
}

View File

@ -0,0 +1,32 @@
package llc.arma.ble.app.ui.common
import kotlinx.coroutines.delay
suspend inline fun <T> retryUntilNotNull(
retryDelay: Long = 10_000,
onNewAttempt: (attempt: Int) -> Unit,
block: () -> T?
) : T {
var attempt = 0
var result: T? = null
while (result == null) {
result = block()
if(result == null) {
onNewAttempt(++attempt)
delay(retryDelay)
}
}
return result
}

View File

@ -0,0 +1,64 @@
package llc.arma.ble.app.ui.common
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.widthIn
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RetryingLoadingTemplate(
attempt: Int?,
onCancel: () -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.widthIn(max = 230.dp)
) {
ContainedLoadingIndicator()
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 = onCancel
) {
Text(
text = "Отмена"
)
}
}
}
}

View File

@ -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..<maxLevel step 1){
Surface(
color = LocalContentColor.current.copy(
alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled

View File

@ -0,0 +1,394 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.Image
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import llc.arma.ble.R
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.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
import llc.arma.ble.domain.model.Ble
class WriteFlowContract {
sealed class Event : ViewEvent {
data object OnWrite : Event()
data object OnNavigateUp : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Display(
val items: List<WriteItemData>
) : State()
data object Writing : State()
data object Success : State()
data object Error : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect(){
data object Up : Navigation()
data object UpSuccess : Navigation()
}
}
}
@Composable
fun WriteFlow(
state: WriteFlowContract.State,
onEvent: (event: WriteFlowContract.Event) -> Unit
) {
when(state){
is WriteFlowContract.State.Display -> DisplayState(state, onEvent)
is WriteFlowContract.State.Error -> ErrorState(state, onEvent)
is WriteFlowContract.State.Loading -> LoadingState()
is WriteFlowContract.State.Success -> SuccessState(state, onEvent)
is WriteFlowContract.State.Writing -> WritingState(state, onEvent)
}
}
@Composable
fun LoadingState(){}
data class WriteItemData(
val title: String,
val subtitle: String
)
@Composable
fun DisplayState(
state: WriteFlowContract.State.Display,
onEvent: (event: WriteFlowContract.Event) -> Unit
){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Column (
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.heightIn(max = 400.dp).verticalScroll(rememberScrollState())
) {
val items = state.items
items.forEach {
WriteItem(
shapeType = items.takeShapeType(it),
itemData = it
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
onEvent(WriteFlowContract.Event.OnNavigateUp)
}
) {
Text(
text = "Отменить"
)
}
Button(
onClick = {
onEvent(WriteFlowContract.Event.OnWrite)
}
) {
Text(
text = "Записать"
)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun WritingState(
state: WriteFlowContract.State.Writing,
onEvent: (event: WriteFlowContract.Event) -> Unit
){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.align(Alignment.CenterHorizontally)
){
ContainedLoadingIndicator()
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
onEvent(WriteFlowContract.Event.OnNavigateUp)
}
) {
Text(
text = "Отменить"
)
}
Button(
enabled = false,
onClick = {}
) {
Text(
text = "Записать"
)
}
}
}
}
@Composable
fun ErrorState(
state: WriteFlowContract.State.Error,
onEvent: (event: WriteFlowContract.Event) -> Unit
){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
.align(Alignment.CenterHorizontally)
){
Image(
painter = painterResource(R.drawable.ic_error),
contentDescription = null,
modifier = Modifier.weight(1f).aspectRatio(1f),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
onEvent(WriteFlowContract.Event.OnNavigateUp)
}
) {
Text(
text = "Отменить"
)
}
Button(
onClick = {
onEvent(WriteFlowContract.Event.OnWrite)
}
) {
Text(
text = "Повторить"
)
}
}
}
}
@Composable
fun SuccessState(
state: WriteFlowContract.State.Success,
onEvent: (event: WriteFlowContract.Event) -> Unit
){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.padding(8.dp)
.align(Alignment.CenterHorizontally)
){
Image(
painter = painterResource(R.drawable.ic_done),
contentDescription = null,
modifier = Modifier.weight(1f).aspectRatio(1f),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
Button(
onClick = {
onEvent(WriteFlowContract.Event.OnNavigateUp)
}
) {
Text(
text = "Ок"
)
}
}
}
}
@Composable
private fun WriteItem(
shapeType: ShapeType,
itemData: WriteItemData
){
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = shapeType.shape
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(text = itemData.title)
Text(
style = MaterialTheme.typography.bodyMedium,
text = itemData.subtitle
)
}
}
}
}

View File

@ -1,62 +0,0 @@
package llc.arma.ble.app.ui.mapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import javax.inject.Inject
class BleMapper @Inject constructor(
private val txMapper: TxMapper
) : Mapper<Ble, BleView> {
override fun map(input: Ble): BleView {
return when(input){
is Ble.Beacon -> {
BleView.Beacon(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
)
)
}
is Ble.Thermometer -> {
BleView.Thermometer(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
thermometerState = BleView.Thermometer.ThermometerState(
temperature = BleView.Thermometer.ThermometerState.TemperatureState(input.thermometerState.temperature, false),
historyInterval = input.thermometerState.historyInterval,
saveHistory = input.thermometerState.saveHistory
)
)
}
is Ble.Accelerometer -> {
BleView.Accelerometer(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = BleView.Accelerometer.AccelerometerState(
saveHistorySettings = input.accelerometerState.saveHistorySettings,
historyInterval = input.accelerometerState.historyInterval
)
)
}
is Ble.Host -> {
BleView.Host(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
hostState = BleView.Host.HostState(
historyInterval = input.hostState.historyInterval
)
)
}
}
}
}

View File

@ -1,62 +0,0 @@
package llc.arma.ble.app.ui.mapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import javax.inject.Inject
class BleViewMapper @Inject constructor(
private val txMapper: TxViewMapper
) : Mapper<BleView, Ble> {
override fun map(input: BleView): Ble {
return when(input){
is BleView.Beacon -> {
Ble.Beacon(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
)
)
}
is BleView.Thermometer -> {
Ble.Thermometer(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
thermometerState = Ble.Thermometer.ThermometerState(
temperature = input.thermometerState.temperature.value,
historyInterval = input.thermometerState.historyInterval,
saveHistory = input.thermometerState.saveHistory
)
)
}
is BleView.Accelerometer -> {
Ble.Accelerometer(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = Ble.Accelerometer.AccelerometerState(
saveHistorySettings = input.accelerometerState.saveHistory,
historyInterval = input.accelerometerState.historyInterval,
)
)
}
is BleView.Host -> {
Ble.Host(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
hostState = Ble.Host.HostState(
historyInterval = input.hostState.historyInterval
)
)
}
}
}
}

View File

@ -1,9 +0,0 @@
package llc.arma.ble.app.ui.mapper
interface Mapper<I, O> {
fun map(input: I): O
fun map(input: List<I>): List<O> = input.map { map(it) }
}

View File

@ -1,23 +0,0 @@
package llc.arma.ble.app.ui.mapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import javax.inject.Inject
class TxMapper @Inject constructor() : Mapper<Ble.BleState.TX, BleView.BleState.TX> {
override fun map(input: Ble.BleState.TX): BleView.BleState.TX {
return when(input){
Ble.BleState.TX.MINUS_40 -> BleView.BleState.TX.MINUS_40
Ble.BleState.TX.MINUS_20 -> BleView.BleState.TX.MINUS_20
Ble.BleState.TX.MINUS_16 -> BleView.BleState.TX.MINUS_16
Ble.BleState.TX.MINUS_12 -> BleView.BleState.TX.MINUS_12
Ble.BleState.TX.MINUS_8 -> BleView.BleState.TX.MINUS_8
Ble.BleState.TX.MINUS_4 -> BleView.BleState.TX.MINUS_4
Ble.BleState.TX.ZERO -> BleView.BleState.TX.ZERO
Ble.BleState.TX.PLUS_3 -> BleView.BleState.TX.PLUS_3
Ble.BleState.TX.PLUS_4 -> BleView.BleState.TX.PLUS_4
}
}
}

View File

@ -1,24 +0,0 @@
package llc.arma.ble.app.ui.mapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import javax.inject.Inject
class TxViewMapper @Inject constructor() : Mapper<BleView.BleState.TX, Ble.BleState.TX> {
override fun map(input: BleView.BleState.TX): Ble.BleState.TX {
return when(input){
BleView.BleState.TX.MINUS_40 -> Ble.BleState.TX.MINUS_40
BleView.BleState.TX.MINUS_20 -> Ble.BleState.TX.MINUS_20
BleView.BleState.TX.MINUS_16 -> Ble.BleState.TX.MINUS_16
BleView.BleState.TX.MINUS_12 -> Ble.BleState.TX.MINUS_12
BleView.BleState.TX.MINUS_8 -> Ble.BleState.TX.MINUS_8
BleView.BleState.TX.MINUS_4 -> Ble.BleState.TX.MINUS_4
BleView.BleState.TX.ZERO -> Ble.BleState.TX.ZERO
BleView.BleState.TX.PLUS_3 -> Ble.BleState.TX.PLUS_3
BleView.BleState.TX.PLUS_4 -> Ble.BleState.TX.PLUS_4
}
}
}

View File

@ -1,112 +0,0 @@
package llc.arma.ble.app.ui.model
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
sealed class BleView(
val info: BleInfo
) {
class Accelerometer(
info: BleInfo,
val state: BleState,
val accelerometerState: AccelerometerState
) : BleView(info) {
class AccelerometerState(
saveHistorySettings: Ble.Accelerometer.HistorySettings,
historyInterval: Long,
) {
var saveHistory by mutableStateOf(saveHistorySettings)
var historyInterval by mutableStateOf(historyInterval)
}
}
class Beacon(
info: BleInfo,
val state: BleState
) : BleView(info)
class Thermometer(
info: BleInfo,
val state: BleState,
val thermometerState: ThermometerState
) : BleView(info) {
class ThermometerState(
temperature: TemperatureState,
saveHistory: Boolean,
historyInterval: Long
) {
class TemperatureState(
val value: Float,
val loading: Boolean
)
var temperature by mutableStateOf(temperature)
var saveHistory by mutableStateOf(saveHistory)
var historyInterval by mutableStateOf(historyInterval)
}
}
class Host(
info: BleInfo,
val state: BleState,
val hostState: HostState
) : BleView(info) {
class HostState(
historyInterval: Long,
) {
var historyInterval by mutableStateOf(historyInterval)
}
}
class BleState(
tx: TX
){
var tx by mutableStateOf(tx)
enum class TX(val value: Int) {
MINUS_40(-40),
MINUS_20(-20),
MINUS_16(-16),
MINUS_12(-12),
MINUS_8(-8),
MINUS_4(-4),
ZERO(0),
PLUS_3(3),
PLUS_4(4);
val powerPercentage: Int
get() {
return when(this){
MINUS_40 -> 1
MINUS_20 -> 5
MINUS_16 -> 7
MINUS_12 -> 10
MINUS_8 -> 16
MINUS_4 -> 20
ZERO -> 40
PLUS_3 -> 80
PLUS_4 -> 100
}
}
}
}
}

View File

@ -1,8 +1,8 @@
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.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -16,8 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.Key
import androidx.compose.material.icons.rounded.NetworkCell
import androidx.compose.material.icons.rounded.ShortText
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@ -26,151 +25,166 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.screen.ble.icon
import llc.arma.ble.app.ui.screen.ble.localized
import llc.arma.ble.app.ui.screen.locale.icon
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.BleInfo
@Composable
fun BleInfoView(
bleInfo: BleInfo
bleInfo: BleInfo,
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 = "Тип метки",
subtitle = bleInfo.type.localized
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.ShortText,
contentDescription = null
)
},
title = "Наименование",
subtitle = bleInfo.name
BleInfoItem(
shapeType = ShapeType.Middle,
icon = {
Icon(
imageVector = Icons.Rounded.Key,
contentDescription = null
)
},
title = "Адрес",
subtitle = bleInfo.serial
)
SpecDivider()
BleInfoItem(
icon = {
Icon(
imageVector = Icons.Rounded.Key,
contentDescription = null
)
},
title = "Адрес",
subtitle = bleInfo.serial
BleInfoItem(
shapeType = ShapeType.Middle,
icon = {
Icon(
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null
)
},
title = "Заряд батареи",
subtitle = "${bleInfo.batteryLevel} %"
)
SpecDivider()
BleInfoItem(
icon = {
Icon(
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null
)
},
title = "Заряд батареи",
subtitle = "${bleInfo.batteryLevel} %"
BleInfoItem(
shapeType = ShapeType.End,
icon = {
Icon(
imageVector = Icons.Rounded.NetworkCell,
contentDescription = null
)
SpecDivider()
BleInfoItem(
icon = {
Icon(
imageVector = Icons.Rounded.NetworkCell,
contentDescription = null
)
},
title = "Мощность сигнала",
subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала"
)
}
}
},
title = "Мощность сигнала",
subtitle = if (bleInfo.rssi != null) "${bleInfo.rssi} dBm" else "Нет сигнала"
)
}
}
@Composable
private fun ColumnScope.SpecDivider(){
private fun SpecDivider(){
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(6.dp))
Divider()
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(6.dp))
}
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 <T> List<T>.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
)
}
}

View File

@ -4,15 +4,12 @@ import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import 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()
@ -20,73 +17,42 @@ class BleListContract {
val bleAddress: String
) : Event()
data class OnRssiRangeChanged(
val rssi: ClosedFloatingPointRange<Float>
) : Event()
data class OnBatteryRangeChanged(
val battery: ClosedFloatingPointRange<Float>
) : 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: State.Filter.Field
) : Event()
data class OnSortOrderChanged(
val order: State.Filter.Order
) : Event()
}
data class State(
val connectedBleList: List<ConnectedBleInfo>,
val bleList: List<BleInfo>,
val filter: Filter
val filterIsEmpty: Boolean,
val summary: BleSummary
) : ViewState {
data class Filter(
val sortField: Field = Field.Name,
val sortOrder: Order = Order.Asc,
val name: String = "",
val mac: String = "",
val battery: ClosedFloatingPointRange<Float> = (0f)..(100f),
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-10f),
val bleType: BleInfo.Type? = null
){
enum class Field {
Name, Mac, Distance, Dbm, Battery
}
enum class Order {
Asc, Desc
}
}
data class BleSummary(
val all: Int,
val lowBattery: Int,
val lost: Int,
val active: Int
)
}
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()

View File

@ -1,12 +1,12 @@
package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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
@ -17,212 +17,346 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowRightAlt
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.Battery1Bar
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Link
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Summarize
import androidx.compose.material.icons.rounded.TimerOff
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon
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.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
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.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.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>(start = true)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BleListScreen(
onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
//onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BleListViewModel>()
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.filter,
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(
title = {
Text(text = "Arma BLE")
},
actions = {
Row(
modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
) {
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}")
Text(text = " | ")
Text(
text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
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
)
}
}
)
val filteredData = remember(state.bleList, state.filter) {
state.bleList.filter {
(it.type == state.filter.bleType || state.filter.bleType == null) &&
it.name.contains(state.filter.name) &&
it.serial.contains(state.filter.mac) &&
state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
state.filter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (state.filter.sortField) {
BleListContract.State.Filter.Field.Name -> it.sortedBy { it.name }
BleListContract.State.Filter.Field.Mac -> it.sortedBy { it.serial }
BleListContract.State.Filter.Field.Distance -> it.sortedBy {
10.0.pow(
(it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
).toFloat()
FilledIconToggleButton(
checked = state.filterIsEmpty.not(),
onCheckedChange = {
viewModel.setEvent(BleListContract.Event.OnShowFilter)
}
) {
Icon(
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
}
BleListContract.State.Filter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
BleListContract.State.Filter.Field.Battery -> it.sortedBy { it.batteryLevel }
}
}.let {
when (state.filter.sortOrder) {
BleListContract.State.Filter.Order.Asc -> it
BleListContract.State.Filter.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 = "Метки в области не найдены"
var showSummary by remember { mutableStateOf(false) }
if(state.bleList.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.height(3.dp)
)
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
if(state.bleList.isEmpty()){
items(items = state.connectedBleList){
Box(modifier = Modifier.fillMaxSize()){
Text(
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.titleMedium,
text = "Метки в области не найдены"
)
}
} else {
LazyColumn(
state = scrollState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
item {
SummaryItem(
shape = if(showSummary) ShapeType.Start else ShapeType.Singleton,
onClick = { showSummary = showSummary.not() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
if(showSummary) {
Text("Итоги")
} else {
Text("${state.summary.all} - ${state.summary.active} | ")
Text(
text = "${state.summary.lost}",
color = LocalContentColor.current.copy(alpha = 0.5f)
)
Text(" | ")
Text(
text = "${state.summary.lowBattery}",
color = MaterialTheme.colorScheme.error
)
}
Spacer(
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(if(showSummary) 180f else 0f)
)
}
}
}
if(showSummary) {
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.Summarize,
onClick = { showSummary = true }
) {
Text("Всего: ${state.summary.all}")
}
}
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.Bluetooth
) {
Text("Активные: ${state.summary.active}")
}
}
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.TimerOff,
contentColor = LocalContentColor.current.copy(alpha = 0.7f)
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "Потерянные: ${state.summary.lost}"
)
}
}
item {
SummaryItem(
contentColor = MaterialTheme.colorScheme.error,
shape = ShapeType.End,
icon = Icons.Rounded.Battery1Bar
) {
Text("Разряженные: ${state.summary.lowBattery}")
}
}
}
item {
Spacer(
modifier = Modifier.height(12.dp)
)
}
items(
items = state.bleList,
key = { it.serial }
) {
BleItem(
ble = it,
shapeType = state.bleList.takeShapeType(it),
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
)
ConnectedBleItem(ble = it) {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
}
}
}
}
items(items = filteredData) {
}
BleItem(
ble = it,
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
@Composable
fun SummaryItem(
shape: ShapeType,
icon: ImageVector? = null,
contentColor: Color = Color.Unspecified,
onClick: (() -> Unit)? = null,
label: @Composable () -> Unit
){
Surface(
shape = shape.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.clickable { onClick?.invoke() }.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if(icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
label()
}
}
}
@ -236,7 +370,7 @@ fun ItemIcon(
Surface(
modifier = Modifier.size(40.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.surfaceContainerHighest,
shape = CircleShape
) {
Box(modifier = Modifier.fillMaxSize()) {
@ -260,20 +394,21 @@ private fun Int.toSignalLevel(): Int {
@Composable
fun BleItem(
shapeType: ShapeType,
ble: BleInfo,
checked: Boolean,
onClick: () -> Unit
){
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.background
MaterialTheme.colorScheme.surfaceContainer
}
val highAlpha = ContentAlpha.high
val disabledAlpha = ContentAlpha.disabled
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
@ -283,41 +418,131 @@ fun BleItem(
LaunchedEffect(ble.scanTime) {
while(true) {
time = SystemClock.elapsedRealtime()
delay(100)
delay(1000)
}
}
var alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){
val alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){
disabledAlpha
} else {
highAlpha
}
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 12.dp
)
) {
BleItemContent(
ble = ble,
color = color,
time = time,
)
Checkbox(
checked = checked,
onCheckedChange = null
)
}
}
}
@Composable
fun BleItem(
shapeType: ShapeType,
ble: BleInfo,
onClick: () -> Unit
){
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.surfaceContainer
}
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
)
}
LaunchedEffect(ble.scanTime) {
while(true) {
time = SystemClock.elapsedRealtime()
delay(1000)
}
}
val alpha = if(time - ble.scanTime > 10_000){
ContentAlpha.disabled
} else {
ContentAlpha.high
}
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
BleItemContent(
ble = ble,
color = color,
time = time,
modifier = Modifier.padding(
horizontal = 16.dp, vertical = 12.dp
)
)
}
}
@Composable
private fun BleItemContent(
ble: BleInfo,
color: Color,
time: Long,
modifier: Modifier = Modifier
){
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(color)
.clickable { onClick() }
.padding(vertical = 8.dp, horizontal = 16.dp)
.alpha(alpha)
modifier = modifier.fillMaxWidth()
) {
Box {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = ble.type.icon,
contentDescription = null
)
}
if(ble.recordEnabled){
if(ble.tableStatus !== BleInfo.HistoryTableStatus.DISABLED){
Surface(
shape = CircleShape,
@ -327,7 +552,11 @@ fun BleItem(
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.error,
color = when(ble.tableStatus){
BleInfo.HistoryTableStatus.EMPTY -> Color(0xff009116)
BleInfo.HistoryTableStatus.NOT_EMPTY -> MaterialTheme.colorScheme.error
else -> Color.Transparent
},
modifier = Modifier
.size(12.dp)
.padding(2.dp)
@ -345,35 +574,76 @@ fun BleItem(
Text(text = ble.name)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
Text(
style = MaterialTheme.typography.bodyMedium,
text = String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
text = ble.serial
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
val contentColor = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
}
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null,
tint = contentColor
)
Box {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "100 %",
modifier = Modifier.alpha(0f)
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.batteryLevel.toString() + " %",
color = contentColor
)
}
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
val distance = remember(ble.rssi, ble.tx) {
String.format("%.2f", (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)
@ -382,7 +652,7 @@ fun BleItem(
SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
Spacer(modifier = Modifier.width(4.dp))
Box {
Text(
@ -400,42 +670,6 @@ fun BleItem(
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
}
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null,
tint = color
)
Box {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "100 %",
modifier = Modifier.alpha(0f)
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.batteryLevel.toString() + " %",
color = color
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
@ -444,7 +678,7 @@ fun BleItem(
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.ArrowRightAlt,
imageVector = Icons.AutoMirrored.Rounded.ArrowRightAlt,
contentDescription = null
)
@ -475,68 +709,8 @@ 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() }
.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 = "Соединено"
)
}
}
}

View File

@ -1,86 +1,33 @@
package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
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.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 javax.inject.Inject
import kotlin.math.pow
@HiltViewModel
class BleListViewModel @Inject constructor(
getBleAroundFlow: GetBleAroundFlow
private val getFilterFlow: GetFilterFlow,
private val getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() {
init {
viewModelScope.launch {
var job: Job? = null
getBleAroundFlow().fold(
onSuccess = {
it.onEach {
setState {
copy(
connectedBleList = emptyList(),
bleList = it
)
}
}.launchIn(viewModelScope)
},
onFailure = {
throw IllegalStateException()
}
)
/*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(), BleListContract.State.Filter())
BleListContract.State(emptyList(), false, BleListContract.State.BleSummary(0,0,0,0))
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)
}
}
@ -88,112 +35,30 @@ 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(
filter = this.filter.copy(mac = event.mac)
)
}
}
state.bleList.firstOrNull { it.serial == event.bleAddress }?.let { ble ->
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnSortOrderChanged
) {
setState {
copy(
filter = this.filter.copy(sortOrder = event.order)
)
}
}
setEffect {
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnSortFieldChanged
) {
setState {
copy(
filter = this.filter.copy(sortField = event.field)
)
}
}
when (ble.type) {
BleInfo.Type.HOST ->
BleListContract.Effect.Navigation.Gate(serial = event.bleAddress)
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnNameFilterChanged
) {
setState {
copy(
filter = this.filter.copy(name = event.name)
)
}
}
BleInfo.Type.BEACON ->
BleListContract.Effect.Navigation.Beacon(serial = event.bleAddress)
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnResetFilter
) {
BleInfo.Type.THERMOMETER ->
BleListContract.Effect.Navigation.Thermometer(serial = event.bleAddress)
BleInfo.Type.ACCELEROMETER ->
BleListContract.Effect.Navigation.Accelerometer(serial = event.bleAddress)
}
}
setState {
copy(
filter = BleListContract.State.Filter()
)
}
setEffect {
BleListContract.Effect.HideFilter
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnRssiRangeChanged
) {
setState {
copy(
filter = this.filter.copy(rssi = event.rssi)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnBatteryRangeChanged
) {
setState {
copy(
filter = this.filter.copy(battery = event.battery)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnTypeChanged
) {
setState {
copy(
filter = this.filter.copy(bleType = event.type)
)
}
}
private fun reduce(
@ -201,8 +66,64 @@ class BleListViewModel @Inject constructor(
event: BleListContract.Event.OnShowFilter
) {
setEffect {
BleListContract.Effect.ShowFilter
BleListContract.Effect.Navigation.BleFilter
}
}
private var scannerJob: Job? = null
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnResetScanner
) {
scannerJob?.cancel()
scannerJob = getFilterFlow().combine(getBleAroundFlow()){ filter, ble ->
val bleList = ble.filter {
(it.type == filter.bleType || filter.bleType == null) &&
it.name.contains(filter.name) &&
it.serial.contains(filter.mac) &&
filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
filter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (filter.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 (filter.sortOrder) {
BleFilter.Order.Asc -> it
BleFilter.Order.Desc -> it.reversed()
}
}
setState {
BleListContract.State(
bleList,
filter == BleFilter(),
BleListContract.State.BleSummary(
all = ble.size,
active = ble.filter { it.batteryLevel == 100 }
.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size,
lost = ble.count { SystemClock.elapsedRealtime() - it.scanTime > 10_000 },
lowBattery = ble.count { it.batteryLevel < 100 }
)
)
}
}.launchIn(viewModelScope)
}
}

View File

@ -1,571 +0,0 @@
package llc.arma.ble.app.ui.screen.ble
import androidx.compose.foundation.background
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.CircleShape
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.Info
import androidx.compose.material.icons.rounded.Nfc
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.material.icons.rounded.Speed
import androidx.compose.material.icons.rounded.Thermostat
import androidx.compose.material.icons.rounded.Warning
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.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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import llc.arma.ble.domain.model.BleInfo
val BleListContract.State.Filter.Order.localized: String
get() {
return when(this){
BleListContract.State.Filter.Order.Asc -> "Прямой ↓"
BleListContract.State.Filter.Order.Desc -> "Обратный ↑"
}
}
val BleListContract.State.Filter.Field.localized: String
get() {
return when(this){
BleListContract.State.Filter.Field.Name -> "Имя"
BleListContract.State.Filter.Field.Mac -> "MAC"
BleListContract.State.Filter.Field.Distance -> "Расстояние"
BleListContract.State.Filter.Field.Dbm -> "dBm"
BleListContract.State.Filter.Field.Battery -> "Заряд"
}
}
val BleInfo.Type?.localized: String
get() {
return when(this){
BleInfo.Type.HOST -> "Хост"
BleInfo.Type.BEACON -> "Маяк"
BleInfo.Type.THERMOMETER -> "Термодатчик"
BleInfo.Type.ACCELEROMETER -> "Акселерометр"
null -> "Все"
}
}
val BleInfo.Type?.icon: ImageVector
get() {
return when(this){
BleInfo.Type.BEACON -> Icons.Rounded.Nfc
BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
BleInfo.Type.HOST -> Icons.Rounded.Info
else -> Icons.Rounded.Warning
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Filter(
filter: BleListContract.State.Filter,
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
.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
}
) {
BleListContract.State.Filter.Field.values().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
}
) {
BleListContract.State.Filter.Order.values().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<BleInfo.Type?>(null).apply {
addAll(BleInfo.Type.values())
}.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(20.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(BleListContract.Event.OnHideFilter)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer,
onClick = {
onEvent(BleListContract.Event.OnResetFilter)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.labelLarge,
text = "Сбросить"
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}

View File

@ -1,124 +0,0 @@
package llc.arma.ble.app.ui.screen.connection
import android.os.Parcelable
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.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.common.BleException
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 ConnectionContract {
sealed class Event : ViewEvent {
data object RefreshBle : Event()
data object OnNavigateUp : Event()
data class OnBeaconNavigationEvent(
val event: BeaconContract.Effect.Navigation
) : Event()
data class OnHostNavigationEvent(
val event: HostContract.Effect.Navigation
) : Event()
data class OnThermometerNavigationEvent(
val event: ThermometerContract.Effect.Navigation
) : Event()
data class OnAccelNavigationEvent(
val event: AccelerometerContract.Effect.Navigation
) : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class DisplayException(
val exception: BleException
) : State()
data class Display(
val ble: Ble
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object NavigateUp : Navigation()
data class NavigateToChangePassword(
val serial: String
) : Navigation()
data class NavigateToRotationsStatistic(
val serial: String
) : Navigation()
}
sealed class InnerNavigation : Effect() {
@Parcelize
data class NavigateToAccelHistory(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToAccelRealtime(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToAccelSpectre(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToHostHistory(
val ble: BleInfo
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateHostToBleTable(
val serial: String
) : InnerNavigation(), Parcelable
}
}
}

View File

@ -1,343 +0,0 @@
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.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.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
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.Surface
import androidx.compose.material3.Text
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.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.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, ExperimentalAnimationApi::class)
@Composable
fun ConnectionScreen(
onNavigationEvent: (ConnectionContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<ConnectionViewModel>()
val state = viewModel.viewState.value
var innerScreen by rememberSaveable {
mutableStateOf<ConnectionContract.Effect.InnerNavigation?>(null)
}
BackHandler(innerScreen != null) {
innerScreen = null
}
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is ConnectionContract.Effect.Navigation -> onNavigationEvent(it)
is ConnectionContract.Effect.InnerNavigation -> {
innerScreen = it
}
}
}.launchIn(this)
}
Box {
Column {
CenterAlignedTopAppBar(
navigationIcon = {
IconButton(
onClick = {
if(innerScreen != null) {
innerScreen = null
} else {
viewModel.setEvent(ConnectionContract.Event.OnNavigateUp)
}
},
content = {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
)
},
title = {
AnimatedContent(
targetState = when (state) {
is ConnectionContract.State.Display -> state.ble.info.name
is ConnectionContract.State.DisplayException -> "Исключение"
is ConnectionContract.State.Loading -> "Соединение.."
},
transitionSpec = {
(slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()).using(
SizeTransform(clip = false)
)
}
) { targetText ->
Text(
text = targetText
)
}
}
)
when (state) {
is ConnectionContract.State.DisplayException -> DisplayException(
onEvent = {
viewModel.setEvent(it)
}
)
is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> {
when (state.ble) {
is Ble.Beacon -> BeaconScreen(
ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnBeaconNavigationEvent(
it
)
)
}
)
is Ble.Thermometer -> {
Column(modifier = Modifier.weight(1f)) {
ThermometerScreen(
ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnThermometerNavigationEvent(it)
)
}
)
}
}
is Ble.Accelerometer -> {
AccelerometerScreen(ble = state.ble) {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
}
}
is Ble.Host -> {
HostScreen(
ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnHostNavigationEvent(it)
)
}
)
}
}
}
}
}
innerScreen?.let {
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
when(it){
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory -> {
AccelerometerHistory(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
){
onNavigationEvent(ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic(it.ble.serial))
}
}
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime -> {
AccelerometerRealtime(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
)
}
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre -> {
AccelerometerSpectre(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
)
}
is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> {
HostHistory(
ble = it.ble,
onDismiss = {
innerScreen = null
}
)
}
is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> {
BleTableEditScreen(it.serial){
when(it){
BleTableEditContract.Effect.Navigation.NavigateUp -> {
innerScreen = null
}
}
}
}
}
}
}
}
}
@Composable
private fun LoadingState(){
Column {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center
)
)
}
}
}
@Composable
private fun DisplayException(
onEvent: (ConnectionContract.Event) -> Unit
){
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
) {
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
text = "Неудалось соединится с устройством"
)
Spacer(modifier = Modifier.height(18.dp))
Surface(
modifier = Modifier
.height(42.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(ConnectionContract.Event.RefreshBle)
}
) {
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelLarge,
text = "Повторить"
)
}
}
}
}
}

View File

@ -1,216 +0,0 @@
package llc.arma.ble.app.ui.screen.connection
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
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.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
@HiltViewModel
class ConnectionViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
) : BaseViewModel<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
init {
refreshBle()
}
override fun setInitialState() = ConnectionContract.State.Loading
override fun handleEvents(event: ConnectionContract.Event) {
when(event){
is ConnectionContract.Event.OnBeaconNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.OnHostNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.RefreshBle -> reduce(viewState.value, event)
is ConnectionContract.Event.OnAccelNavigationEvent -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnHostNavigationEvent
) {
when(event.event){
HostContract.Effect.Navigation.NavigateUp -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
HostContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is HostContract.Effect.Navigation.NavigateToHostHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble
)
}
}
is HostContract.Effect.Navigation.NavigateToBleTable -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial
)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnBeaconNavigationEvent
) {
when(event.event){
BeaconContract.Effect.Navigation.NavigateUp -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
BeaconContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnThermometerNavigationEvent
) {
when(event.event){
ThermometerContract.Effect.Navigation.NavigateUp -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
ThermometerContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
when(event.event){
AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnNavigateUp
) {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.RefreshBle
) {
refreshBle()
}
private fun refreshBle(){
val serial = savedStateHandle.get<String>("serial")
if(serial != null){
viewModelScope.launch {
setState {
ConnectionContract.State.Loading
}
getBleBySerial(serial).fold(
onSuccess = {
it.onEach {
setState {
ConnectionContract.State.Display(
ble = it
)
}
}.launchIn(viewModelScope)
},
onFailure = {
setState {
ConnectionContract.State.DisplayException(it)
}
}
)
}
} else {
throw IllegalArgumentException("serial arg must not be null")
}
}
}

View File

@ -0,0 +1,45 @@
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 origin: BleFilter,
val filter: BleFilter
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect(){
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,634 @@
package llc.arma.ble.app.ui.screen.filter
import androidx.compose.animation.animateContentSize
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<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BleFilterScreen(
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BleFilterViewModel>()
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 {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
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 = remember(state.filter.sortField) {
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 = remember(state.filter.sortOrder) {
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 = remember(state.filter.bleType) {
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<BleInfo.Type?>(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 {
var sliderState by remember(state.filter.rssi) {
mutableStateOf(state.filter.rssi)
}
RangeSlider(
value = sliderState,
onValueChange = {
sliderState = it
},
onValueChangeFinished = {
viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(rssi = sliderState)
)
)
},
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
) {
var sliderState by remember(state.filter.battery) {
mutableStateOf(state.filter.battery)
}
RangeSlider(
value = sliderState,
onValueChange = {
sliderState = it
},
onValueChangeFinished = {
viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(battery = sliderState)
)
)
},
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() + " %"
)
}
}
}
}
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.filter != state.origin) {
Button(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnSave)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Применить"
)
}
}
}
}
}

View File

@ -0,0 +1,106 @@
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<BleFilterContract.State, BleFilterContract.Event, BleFilterContract.Effect>() {
init {
viewModelScope.launch {
val filter = getFilterFlow.invoke().firstOrNull() ?: BleFilter()
setState { BleFilterContract.State.Display(filter, 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,
) {
if(state is BleFilterContract.State.Display) {
setState {
state.copy(
filter = event.filter
)
}
}
}
private fun reduce(
state: BleFilterContract.State,
event: BleFilterContract.Event.OnResetFilter,
) {
viewModelScope.launch {
saveFilter(BleFilter())
setEffect {
BleFilterContract.Effect.Navigation.Up
}
}
}
}

View File

@ -1,216 +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 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()
}
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 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()
}
}
}

View File

@ -1,419 +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.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.PowerEdit
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
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AccelerometerScreen(
ble: Ble.Accelerometer,
onEvent: (AccelerometerContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
LaunchedEffect(ble){
viewModel.setEvent(AccelerometerContract.Event.OnBleChanged(ble))
}
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(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) {
PowerEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
}
SheetPage.ACCEL_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.SPECTRE_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.FREQUENCY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFrequencyEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.AXIS_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftAxisEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.FFT_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftModeEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
IntervalEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.ACCEL_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.SPECTRE_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.ACCEL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
HistoryEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
null -> {
bottomDialog.hide()
}
SheetPage.ACCEL_REALTIME_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelRealtimeViewEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
}
}
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)
}
}
}.launchIn(this)
}
Column {
when(state){
is AccelerometerContract.State.Display -> {
DisplayState(
origin = state.origin,
ble = state.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
is AccelerometerContract.State.Loading -> LoadingState()
}
}
}

View File

@ -1,672 +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<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
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)
}
}
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,
)
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
)
}
}
)
}
}
}
}
}
}

View File

@ -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<RootGraph>(style = DestinationStyle.Dialog::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerHistoryForm(
resultNavigator: ResultBackNavigator<AccelerometerHistoryFormData>
) {
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 = "Сохранить"
)
}
}
}
}
}

View File

@ -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<List<Ble.Accelerometer.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}

View File

@ -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<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
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)
}
}
}

View File

@ -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
@ -12,7 +12,7 @@ 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.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.CloudUpload
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.TableView
@ -23,20 +23,23 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
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
@ -48,30 +51,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 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 llc.arma.ble.domain.usecase.GetBleBySerial
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class AccelEntry(
val localDate: Long,
@ -83,49 +70,43 @@ class AccelEntry(
}
@Destination<RootGraph>
@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<AccelerometerHistoryViewModel>()
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)
)
}
/*DisposableEffect("ble") {
onDispose {
viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure)
}
}*/
Column() {
Column {
TopAppBar(
navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.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})"
@ -134,7 +115,7 @@ fun AccelerometerHistory(
}
}
AccelerometerHistoryContract.State.Exception -> accelMode.localized
}
}*/ ""
Text(
modifier = Modifier.weight(1f),
@ -145,7 +126,7 @@ fun AccelerometerHistory(
actions = {
IconButton(
onClick = onShowStatistic,
onClick = {} ,
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false
@ -174,7 +155,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
@ -194,8 +177,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()
}
}
@ -210,13 +193,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) {
@ -348,6 +332,12 @@ fun Display(
guideline = axisGuidelineComponent()
)
val axis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
)
when(lastMeasure){
is Ble.Accelerometer.HistoryPoint.Acceleration,
is Ble.Accelerometer.HistoryPoint.Angle -> {
@ -359,11 +349,7 @@ fun Display(
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
bottomAxis = axis,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
@ -382,11 +368,7 @@ fun Display(
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
bottomAxis = axis,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
@ -405,11 +387,7 @@ fun Display(
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
bottomAxis = axis,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
@ -509,7 +487,7 @@ fun Display(
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
@ -522,7 +500,7 @@ fun Display(
}
@Composable
private fun Exception() {
private fun ErrorState() {
Box(
modifier = Modifier
.padding(8.dp)
@ -540,179 +518,5 @@ private fun Exception() {
}
class AccelerometerHistoryContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
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<List<Ble.Accelerometer.HistoryPoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
private val exportToXlsx: ExportToXlsx,
private val getBleBySerial: GetBleBySerial
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
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)
}
}
}

View File

@ -0,0 +1,107 @@
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.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
class AccelerometerContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object OnRestart : Event()
data object OnShowAccelerometerHistory : Event()
data object OnShowRealtimeForm : Event()
data object OnWriteBle : Event()
data object OnChangePassword : Event()
data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data object OnReadIntervalEdit : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
data object OnPowerEdit : Event()
data class OnPowerChanged(
val tx: Ble.BleState.TX
) : Event()
data object OnShowHistoryForm : Event()
data object OnDisableSaveHistory : Event()
data class OnEnableSaveHistory(
val mode: AccelViewMode,
val scale: AccelScale
) : Event()
}
sealed class State : ViewState {
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: Ble.Accelerometer
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data class Write(
val serial: String,
val request: Ble.Accelerometer.WriteRequest
) : 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: Ble.BleState.TX
) : Navigation()
data class ChangePassword(
val serial: String
) : Navigation()
data class AccelHistory(
val serial: String
) : Navigation()
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,176 @@
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.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
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.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.AccelerometerWriteScreenDestination
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.flow.launchIn
import kotlinx.coroutines.flow.onEach
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.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerScreen(
navigator: DestinationsNavigator,
bleSerial: String,
historyFormResult: ResultRecipient<AccelerometerHistoryFormDestination, AccelerometerHistoryFormData>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
writeResult: ResultRecipient<AccelerometerWriteScreenDestination, Boolean>,
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value
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()))
}
}
writeResult.onResult {
if(it) viewModel.setEvent(AccelerometerContract.Event.OnRestart)
}
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
is AccelerometerContract.Effect.Navigation.AccelHistory ->
navigator.navigate(AccelerometerHistoryDestination(it.serial))
is AccelerometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.serial))
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "ReadIntervalSelector",
duration = it.interval,
minimum = 1000
))
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))
is AccelerometerContract.Effect.Navigation.Write ->
navigator.navigate(AccelerometerWriteScreenDestination(
bleSerial = it.serial,
writeRequest = it.request
))
AccelerometerContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}.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)
},
actions = {
if(state is AccelerometerContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(AccelerometerContract.Event.OnRestart)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when(state){
is AccelerometerContract.State.Display -> {
DisplayState(viewModel, state)
}
is AccelerometerContract.State.Loading -> LoadingState(
viewModel, state
)
}
}
}
}

View File

@ -0,0 +1,357 @@
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.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
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 AccelerometerViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
init {
loadData()
}
override fun setInitialState() = AccelerometerContract.State.Loading(null)
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.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.OnShowRealtimeForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnRestart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnNavigateUp
) {
setEffect {
AccelerometerContract.Effect.Navigation.Up
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnRestart
) {
loadData()
}
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) {
setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
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) {
setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
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) {
setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
saveHistorySettings = Ble.Accelerometer.HistorySettings.Disabled
)
)
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnEnableSaveHistory
) {
if(state is AccelerometerContract.State.Display) {
setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
saveHistorySettings = 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(
serial = state.accelerometer.info.serial
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
val newBle = state.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,
)
setEffect {
AccelerometerContract.Effect.Navigation.Write(state.accelerometer.info.serial, writeRequest)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerChanged
) {
if(state is AccelerometerContract.State.Display) {
setState {
state.copy(
accelerometer = state.accelerometer.copy(
state = state.accelerometer.state.copy(
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 var loadJob: Job? = null
private fun loadData(){
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState {
AccelerometerContract.State.Loading(null)
}
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
AccelerometerContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if( ble is Ble.Accelerometer){
setState {
when(this){
is AccelerometerContract.State.Display -> {
copy(
origin = Ble.Accelerometer(
info = ble.info,
state = origin.state,
accelerometerState = origin.accelerometerState
)
)
}
is AccelerometerContract.State.Loading -> {
AccelerometerContract.State.Display(
origin = ble,
accelerometer = ble
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,26 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import kotlinx.serialization.Serializable
import llc.arma.ble.domain.usecase.AccelViewMode
@Serializable
sealed class RealtimeViewMode {
companion object {
val entries: List<RealtimeViewMode>
get() {
return AccelViewMode.entries.map {
Accel(it)
} + Spectre
}
}
data class Accel(
val accelViewMode: AccelViewMode
): RealtimeViewMode()
data object Spectre : RealtimeViewMode()
}

View File

@ -0,0 +1,214 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Switch
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.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.accelerometer.main.AccelerometerViewModel
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.app.ui.screen.locale.value
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(
viewModel: AccelerometerViewModel,
state: AccelerometerContract.State.Display
) {
val scrollState = rememberScrollState()
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
.weight(1f)
) {
BleInfoView(
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${state.accelerometer.state.tx.value} db",
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(AccelerometerContract.Event.OnPowerEdit)
}
val history = state.accelerometer.accelerometerState.saveHistorySettings
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 = state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled,
onCheckedChange = {
if(it){
viewModel.setEvent(AccelerometerContract.Event.OnShowHistoryForm)
} else {
viewModel.setEvent(AccelerometerContract.Event.OnDisableSaveHistory)
}
}
)
}
)
if (state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = state.accelerometer.accelerometerState.historyInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
}
}
if (state.accelerometer.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
if (state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал чтения",
subtitle = state.accelerometer.accelerometerState.readInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(AccelerometerContract.Event.OnReadIntervalEdit)
}
}
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "График измерений",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
when (state.origin.accelerometerState.saveHistorySettings) {
is Ble.Accelerometer.HistorySettings.Disabled ->
viewModel.setEvent(AccelerometerContract.Event.OnShowRealtimeForm)
is Ble.Accelerometer.HistorySettings.Enabled ->
viewModel.setEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
}
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
viewModel.setEvent(AccelerometerContract.Event.OnChangePassword)
}
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.accelerometer) {
Button(
onClick = {
viewModel.setEvent(AccelerometerContract.Event.OnWriteBle)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
}
}
}

View File

@ -0,0 +1,32 @@
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.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LoadingState(
viewModel: AccelerometerViewModel,
state: AccelerometerContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
RetryingLoadingTemplate(state.attempt) {
viewModel.setEvent(AccelerometerContract.Event.OnNavigateUp)
}
}
}

View File

@ -0,0 +1,57 @@
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.AccelViewMode
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object OnRefresh : Event()
}
sealed class State : ViewState {
data class Loading(
val attempt: Int?
) : State()
data class DisplayCommon(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Common>
) : State()
data class DisplayAngle(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Angle>
) : State()
data class DisplayRotation(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Rotation>
) : State()
data class DisplayVibration(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Vibration>
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,151 @@
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.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftFrequency
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<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
private var measureJob: Job? = null
init {
startReadMeasure()
}
override fun setInitialState() = AccelerometerAccelContract.State.Loading(null)
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefresh
) {
startReadMeasure()
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnNavigateUp
) {
setEffect { AccelerometerAccelContract.Effect.Navigation.Up }
}
private fun startReadMeasure() {
val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle)
setState {
AccelerometerAccelContract.State.Loading(null)
}
measureJob?.cancel()
measureJob = viewModelScope.launch {
val flow = retryUntilNotNull(
onNewAttempt = {
setState {
AccelerometerAccelContract.State.Loading(it)
}
}
) {
getAccelerometerMeasureBySerialFlow(
params.bleSerial,
params.accelScale,
params.accelMode,
params.fftAxis,
params.fftMode,
FftFrequency.F_400
).getOrNull()
}
flow.onEach {
val state = viewState.value
val newState = when (it) {
is Ble.Accelerometer.RealtimePoint.Angle -> {
if (state is AccelerometerAccelContract.State.DisplayAngle) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayAngle(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Common -> {
if (state is AccelerometerAccelContract.State.DisplayCommon) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayCommon(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Rotation -> {
if (state is AccelerometerAccelContract.State.DisplayRotation) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayRotation(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Vibration -> {
if (state is AccelerometerAccelContract.State.DisplayVibration) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayVibration(
params.accelMode,
listOf(it)
)
}
}
}
setState { newState }
}.launchIn(this)
}
}
}

View File

@ -0,0 +1,724 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import android.graphics.Color
import androidx.compose.animation.core.tween
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.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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
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.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.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
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.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.component.shape.ShapeComponent
import com.patrykandpatrick.vico.core.component.text.TextComponent
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 com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localized
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
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun AccelerometerRealtime(
bleSerial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when (it) {
AccelerometerAccelContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = navigator::popBackStack
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
text = accelMode.localized,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
if((state is AccelerometerAccelContract.State.Loading).not()) {
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
},
enabled = true
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
}
)
}
) {
Box(
modifier = Modifier.padding(it)
) {
when (state) {
is AccelerometerAccelContract.State.DisplayAngle -> DisplayAngleState(state)
is AccelerometerAccelContract.State.DisplayCommon -> DisplayCommonState(state)
is AccelerometerAccelContract.State.DisplayRotation -> DisplayRotationState(state)
is AccelerometerAccelContract.State.DisplayVibration -> DisplayVibrationState(state)
is AccelerometerAccelContract.State.Loading -> LoadingState(viewModel, state)
}
}
}
}
@Composable
private fun DisplayCommonState(
state: AccelerometerAccelContract.State.DisplayCommon
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.fillMaxSize()
) {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.x)
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.y)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.z)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
lineComponent = ShapeComponent(color = Color.TRANSPARENT),
thresholdValue = 0f
)
),
persistentMarkers = mapOf(
xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Surface(
shape = ShapeType.Start.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = 8.dp)
) {
Text(
text = "Ось X",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.Middle.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = "Ось Y",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.End.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(bottom = 8.dp)
) {
Text(
text = "Ось Z",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
@Composable
private fun DisplayAngleState(
state: AccelerometerAccelContract.State.DisplayAngle
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
strokeCap = StrokeCap.Round
)
} else {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.x )
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.y)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.z)
})
val lastMeasure = state.measureHistory.last()
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось X: ${lastMeasure.x}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
angle = lastMeasure.x,
modifier = Modifier.weight(1f),
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Y: ${lastMeasure.y}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.y
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Z: ${lastMeasure.z}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.z
)
}
}
}
}
}
@Composable
private fun DisplayRotationState(
state: AccelerometerAccelContract.State.DisplayRotation
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
strokeCap = StrokeCap.Round
)
} else {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.angle )
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.tmp)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.turnovers.toFloat())
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
),
persistentMarkers = mapOf(xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
val lastMeasure = state.measureHistory.last()
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Положение: ${lastMeasure.angle}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
angle = lastMeasure.angle,
modifier = Modifier.weight(1f),
)
}
Text(text = "Скорость:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Обороты:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
@Composable
private fun DisplayVibrationState(
state: AccelerometerAccelContract.State.DisplayVibration
) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.value)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
labelComponent = textComponent(color = androidx.compose.ui.graphics.Color.Transparent),
lineComponent = ShapeComponent(color = Color.TRANSPARENT),
thresholdValue = 0f
)
),
persistentMarkers = mapOf(
xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier.fillMaxSize(),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
@Composable
fun Angle(
modifier: Modifier = Modifier,
angle: Float
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Column(
modifier = modifier.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "0°C")
Row(
modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "-90°C")
Surface(
modifier = Modifier
.aspectRatio(1f)
.fillMaxHeight()
.padding(8.dp),
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
) {
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.fillMaxWidth()
.rotate(-90f + angle),
horizontalArrangement = Arrangement.End
) {
Box(modifier = Modifier.weight(1f))
Divider(modifier = Modifier.weight(1f))
}
}
}
Text(text = "90°C")
}
Text(text = "±180°C")
}
}
}
@Composable
private fun LoadingState(
viewModel: AccelerometerAccelViewModel,
state: AccelerometerAccelContract.State.Loading
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxSize(),
){
RetryingLoadingTemplate(
attempt = state.attempt
) {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
}
}
}

View File

@ -0,0 +1,434 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt.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.generated.destinations.AccelerometerRealtimeDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.serialization.Serializable
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.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<RootGraph>(style = DestinationStyle.Dialog::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerRealtimeForm(
navigator: DestinationsNavigator,
bleSerial: String,
) {
var mode by remember { mutableStateOf<RealtimeViewMode>(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 = "Продолжить"
)
}
}
}
}
}

View File

@ -0,0 +1,334 @@
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.Row
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.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.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.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<RootGraph>
@Composable
fun AccelerometerSpectre(
bleSerial: String,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<AccelerometerSpectreViewModel>()
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<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun 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<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp,
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)
)
}
}

View File

@ -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<Ble.Accelerometer.SpectrePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.SpectrePoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}

View File

@ -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<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
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)
}
}
}

View File

@ -1,343 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
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.Surface
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
val FftFrequency.localized: String
get() {
return when(this){
FftFrequency.OFF -> "откл"
FftFrequency.F_1 -> "1 Гц"
FftFrequency.F_10 -> "10 Гц"
FftFrequency.F_25 -> "25 Гц"
FftFrequency.F_50 -> "50 Гц"
FftFrequency.F_100 -> "100 Гц"
FftFrequency.F_200 -> "200 Гц"
FftFrequency.F_400 -> "400 Гц"
FftFrequency.F_1620 -> "1620 Гц"
FftFrequency.F_1344 -> "1344 Гц"
}
}
val FftAxis.localized: String
get() {
return when(this){
FftAxis.AUTO -> "Авто"
FftAxis.X -> "Ось X"
FftAxis.Y -> "Ось Y"
FftAxis.Z -> "Ось Z"
}
}
val FftViewMode.localized: String
get() {
return when(this){
FftViewMode.SPECTRE -> "Спектр"
FftViewMode.X -> "Ось X"
FftViewMode.Y -> "Ось Y"
FftViewMode.Z -> "Ось Z"
}
}
val AccelScale.localized: String
get() {
return when(this){
AccelScale.S_2 -> "2g"
AccelScale.S_4 -> "4g"
AccelScale.S_8 -> "8g"
AccelScale.S_16 -> "16g"
}
}
@Composable
fun 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))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowAccelerometerAccel)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Продолжить"
)
}
}
}
}

View File

@ -1,99 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.FftAxis
@Composable
fun AccelFftAxisEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftAxis = state.fftAxis
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft axis",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftAxis.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftAxisChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftAxis,
onClick = {
onEvent(AccelerometerContract.Event.OnFftAxisChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,99 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelFftModeEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftMode = state.fftViewMode
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftViewMode.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftModeChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftMode,
onClick = {
onEvent(AccelerometerContract.Event.OnFftModeChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,99 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.FftFrequency
@Composable
fun AccelFrequencyEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftFrequency = state.fftFrequency
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft frequency",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftFrequency.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftFrequency,
onClick = {
onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,147 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.AccelViewMode.values
val RealtimeViewMode.localized: String
get() {
return when(this){
is RealtimeViewMode.Accel -> this.accelViewMode.localized
RealtimeViewMode.Spectre -> "Спектр"
}
}
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))
values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { value = RealtimeViewMode.Accel(it) }
.padding(4.dp)
) {
RadioButton(
selected = value is RealtimeViewMode.Accel && it == (value as RealtimeViewMode.Accel).accelViewMode,
onClick = {
value = RealtimeViewMode.Accel(it)
}
)
Text(text = it.localized)
}
}
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))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnRealtimeViewModeChanged(value))
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,131 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
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,
){
var 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.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable {
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
}
}
.padding(4.dp)
) {
RadioButton(
selected = it == fftMode,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
}
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelEdit)
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,138 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.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.screen.inspection.accelerometer.AccelerometerContract
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.AccelViewMode.PEAK_ACCELERATION
import llc.arma.ble.domain.usecase.AccelViewMode.RMS
import llc.arma.ble.domain.usecase.AccelViewMode.ROTATIONS
import llc.arma.ble.domain.usecase.AccelViewMode.VIBRATION
import llc.arma.ble.domain.usecase.AccelViewMode.values
val AccelViewMode.localized: String
get() {
return when(this){
ACCELERATION -> "Ускорение"
PEAK_ACCELERATION -> "Пиковое ускорение"
RMS -> "Среднеквадратичное ускорение"
VIBRATION -> "Вибрация"
ANGLE -> "Угол"
ROTATIONS -> "Обороты"
}
}
@Composable
fun AccelViewEdit(
next: AccelerometerContract.Event.Next,
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelViewMode) {
mutableStateOf(state.accelViewMode)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { value = it }
.padding(4.dp)
) {
RadioButton(
selected = it == value,
onClick = {
value = it
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL -> {
onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
AccelerometerContract.Event.Next.HISTORY -> {
onEvent(AccelerometerContract.Event.OnHistoryViewModeChanged(value))
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -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.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.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<AccelerometerSpectreViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial, accelMode, fftAxis, fftMode, frequency) {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
}
DisposableEffect(key1 = "ble", effect = {
onDispose {
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.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<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun Display(
state: AccelerometerSpectreContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp,
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<Ble.Accelerometer.SpectrePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.SpectrePoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerSpectreViewModel @Inject constructor(
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
) : BaseViewModel<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
private var job: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
override fun handleEvents(event: AccelerometerSpectreContract.Event) {
when(event){
is AccelerometerSpectreContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerSpectreContract.State.Display) {
//if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when(state.loadingHistoryState){
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
} else {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerSpectreContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerSpectreContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnRefreshHistory
) {
/*viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}*/
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.StopMeasure
) {
job?.cancel()
}
}

View File

@ -1,704 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.core.tween
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.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.shape.CircleShape
import androidx.compose.material.icons.Icons
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
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.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
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.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
@Composable
fun AccelerometerRealtime(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
onDismiss: (() -> Unit)? = null
) {
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
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.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))
},
enabled = true
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerAccelContract.State.Display -> Display(state = state)
is AccelerometerAccelContract.State.Exception -> Exception()
}
}
}
}
@Composable
fun Display(
state: AccelerometerAccelContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
strokeCap = StrokeCap.Round
)
} else {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.angle )
}
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.y )
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.y)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.tmp)
}
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.turnovers.toFloat())
}
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
),
persistentMarkers = mapOf(xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
val lastMeasure = state.measureHistory.lastOrNull()
when(lastMeasure){
is Ble.Accelerometer.RealtimePoint.Angle -> {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось X: ${lastMeasure.x}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
angle = lastMeasure.x,
modifier = Modifier.weight(1f),
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Y: ${lastMeasure.y}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.y
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Z: ${lastMeasure.z}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.z
)
}
}
}
is Ble.Accelerometer.RealtimePoint.Rotation -> {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Положение: ${lastMeasure.angle}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
angle = lastMeasure.angle,
modifier = Modifier.weight(1f),
)
}
Text(text = "Скорость:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Обороты:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
is Ble.Accelerometer.RealtimePoint.Vibration -> {
Column {
Text(text = "Вибрация:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
is Ble.Accelerometer.RealtimePoint.Common -> {
Column() {
Text(text = "Ось X:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Y:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Z:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
null -> {}
}
}
}
}
@Composable
fun Angle(
modifier: Modifier = Modifier,
angle: Float
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Column(
modifier = modifier.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "0°C")
Row(
modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "-90°C")
Surface(
modifier = Modifier
.aspectRatio(1f)
.fillMaxHeight()
.padding(8.dp),
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
) {
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.fillMaxWidth()
.rotate(-90f + angle),
horizontalArrangement = Arrangement.End
) {
Box(modifier = Modifier.weight(1f))
Divider(modifier = Modifier.weight(1f))
}
}
}
Text(text = "90°C")
}
Text(text = "±180°C")
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
}
sealed class State : ViewState {
data class Display(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerAccelViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
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)
}
}
}
}

View File

@ -1,327 +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.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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
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.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
onEvent: (AccelerometerContract.Event) -> Unit,
origin: Ble.Accelerometer,
ble: BleView.Accelerometer
) {
Column() {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = origin.info)
}
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(AccelerometerContract.Event.OnPowerEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${ble.state.tx.value} db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
val history = ble.accelerometerState.saveHistory
if(history is Ble.Accelerometer.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) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерений"
)
val hours =
ble.accelerometerState.historyInterval / millisInHour
val minutes =
(ble.accelerometerState.historyInterval - (hours * millisInHour)) / millisInMinute
val seconds =
(ble.accelerometerState.historyInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин. $seconds сек."
)
}
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 {
when(origin.accelerometerState.saveHistorySettings){
is Ble.Accelerometer.HistorySettings.Disabled ->
onEvent(AccelerometerContract.Event.OnAccelEdit)
is Ble.Accelerometer.HistorySettings.Enabled ->
onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "График измерений"
)
/*Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = when(origin.accelerometerState.saveHistorySettings){
Ble.Accelerometer.HistorySettings.Disabled -> "Текущие измерения"
is Ble.Accelerometer.HistorySettings.Enabled -> ""
}
)*/
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
}
)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
}
}
}
}

View File

@ -1,173 +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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
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.Surface
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.screen.inspection.accelerometer.AccelerometerContract
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))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnHideHistoryEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -1,265 +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.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.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.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 = 10_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))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
AccelerometerContract.Event.OnSaveIntervalChanged(
value.toLong()
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}
const val 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
)
}
}
}

View File

@ -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))
}
}

View File

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

View File

@ -1,437 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.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.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
import llc.arma.ble.domain.model.Ble
@Composable
fun Write(
state: AccelerometerContract.State.Display.WriteState,
onEvent: (AccelerometerContract.Event) -> Unit
) {
Column(
modifier = Modifier.animateContentSize()
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is AccelerometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.saveHistorySettings != null || state.writeRequest.historyInterval != null) {
state.writeRequest.tx?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName} db"
)
}
}
}
}
state.writeRequest.saveHistorySettings?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
is Ble.Accelerometer.HistorySettings.Enabled -> "Включено"
}
)
}
}
}
}
state.writeRequest.historyInterval?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерений"
)
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин. $seconds сек."
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnWriteBle)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Записать"
)
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
is AccelerometerContract.State.Display.WriteState.Writing -> {
Box {
Column() {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Failure -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,62 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.write
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
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.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.common.WriteFlow
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.write.ThermometerWriteViewModel
import llc.arma.ble.domain.model.Ble
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun AccelerometerWriteScreen(
bleSerial: String,
writeRequest: Ble.Accelerometer.WriteRequest,
navigator: ResultBackNavigator<Boolean>
) {
val viewModel = hiltViewModel<AccelerometerWriteViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
WriteFlowContract.Effect.Navigation.Up ->
navigator.navigateBack()
WriteFlowContract.Effect.Navigation.UpSuccess ->
navigator.navigateBack(true)
}
}
}
Surface(
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier.padding(20.dp)
) {
WriteFlow(
state = state,
onEvent = viewModel::setEvent
)
}
}
}

View File

@ -0,0 +1,149 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.write
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BeaconWriteScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.common.WriteItemData
import llc.arma.ble.app.ui.screen.ShapeType
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
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class AccelerometerWriteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val writeBle: WriteBle
) : BaseViewModel<WriteFlowContract.State, WriteFlowContract.Event, WriteFlowContract.Effect>() {
init {
val params = AccelerometerWriteScreenDestination.argsFrom(savedStateHandle)
val items = mutableListOf<WriteItemData>()
params.writeRequest.tx?.let {
items.add(WriteItemData("Мощность", "${it.localizedName} db"))
}
params.writeRequest.saveHistorySettings?.let {
items.add(
WriteItemData(
title = "Сохранять историю измерений",
subtitle = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
is Ble.Accelerometer.HistorySettings.Enabled -> "Включено"
},
)
)
}
params.writeRequest.historyInterval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
items.add(
WriteItemData(
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
)
}
params.writeRequest.readInterval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
items.add(
WriteItemData(
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
)
}
setState {
WriteFlowContract.State.Display(
items
)
}
}
override fun setInitialState() = WriteFlowContract.State.Loading
override fun handleEvents(event: WriteFlowContract.Event) {
when(event){
is WriteFlowContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is WriteFlowContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnNavigateUp
){
setEffect {
when(state){
is WriteFlowContract.State.Display,
WriteFlowContract.State.Error,
WriteFlowContract.State.Loading,
WriteFlowContract.State.Writing -> WriteFlowContract.Effect.Navigation.Up
WriteFlowContract.State.Success -> WriteFlowContract.Effect.Navigation.UpSuccess
}
}
}
private var writeJob: Job? = null
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnWrite
){
val params = AccelerometerWriteScreenDestination.argsFrom(savedStateHandle)
setState {
WriteFlowContract.State.Writing
}
writeJob?.cancel()
writeJob = viewModelScope.launch {
writeBle(params.bleSerial, params.writeRequest).fold(
onSuccess = {
setState {
WriteFlowContract.State.Success
}
},
onFailure = {
setState {
WriteFlowContract.State.Error
}
}
)
}
}
}

View File

@ -3,82 +3,62 @@ package llc.arma.ble.app.ui.screen.inspection.beacon
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 {
object OnWriteBle : Event()
data object OnNavigateUp : Event()
object OnHideWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event()
object OnShowWriteBlePreview : Event()
object OnPowerEdit : Event()
data object OnPowerEdit : Event()
data class OnBleChanged(
val ble: Ble.Beacon
) : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
data class OnTxChanged(
val tx: Ble.BleState.TX
) : Event()
data class OnTxChanged(val tx: Int) : Event()
object OnNavigateUpClicked : Event()
object OnChangePassword : Event()
data object OnChangePassword : Event()
}
sealed class State : ViewState {
object Loading : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Beacon,
val beacon: BleView.Beacon,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
object Success : WriteState()
object Failure : WriteState()
}
}
val beacon: Ble.Beacon
) : State()
}
sealed class Effect : ViewSideEffect {
object ShowPowerPicker : Effect()
object HidePowerPicker : Effect()
object HideWriteBlePreview : Effect()
object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() {
object NavigateToChangePassword : Navigation()
data class Write(
val bleSerial: String,
val writeRequest: Ble.Beacon.WriteRequest
) : Navigation()
object NavigateUp : Navigation()
data object Up : Navigation()
data class PasswordForm(
val bleSerial: String
) : Navigation()
data class TxSelector(
val tx: Ble.BleState.TX?
) : Navigation()
}

View File

@ -1,134 +1,127 @@
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.material3.CircularProgressIndicator
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
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
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 com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.BeaconWriteScreenDestination
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.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.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.PowerEdit
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write
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.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE, POWER_EDIT
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BeaconScreen(
ble: Ble.Beacon,
onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit
bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BeaconViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
txSelectResult.onResult {
viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
}
val bottomDialog = rememberBottomDialogState()
LaunchedEffect("effect"){
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
is BeaconContract.Effect.Navigation -> onNavigationEvent(it)
BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
BeaconContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
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.navigateUp()
is BeaconContract.Effect.Navigation.Write ->
navigator.navigate(BeaconWriteScreenDestination(it.bleSerial, it.writeRequest))
}
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is BeaconContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
}
)
}
}
SheetPage.POWER_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is BeaconContract.State.Display) {
PowerEdit(
state = currentState.beacon,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Column {
when(state){
is BeaconContract.State.Display -> DisplayState(
onEvent = {
viewModel.setEvent(it)
) {
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(viewModel, state)
is BeaconContract.State.Loading -> LoadingState(viewModel, state)
}
}
}
}
@Composable
private fun LoadingState(){
private fun LoadingState(
viewModel: BeaconViewModel,
state: BeaconContract.State.Loading,
){
Box(modifier = Modifier.fillMaxSize()){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
RetryingLoadingTemplate(state.attempt){
viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
}
}

View File

@ -1,52 +1,87 @@
package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
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 BeaconViewModel @Inject constructor(
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
override fun setInitialState() = BeaconContract.State.Loading
init {
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
BeaconContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if(ble is Ble.Beacon){
setState {
when(this){
is BeaconContract.State.Display -> {
copy(
origin = Ble.Beacon(
info = ble.info,
state = origin.state
)
)
}
is BeaconContract.State.Loading -> {
BeaconContract.State.Display(
origin = ble,
beacon = ble
)
}
}
}
}
}
}
override fun setInitialState() = BeaconContract.State.Loading(null)
override fun handleEvents(event: BeaconContract.Event) {
when(event){
is BeaconContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnBleChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnChangePassword -> reduce(viewState.value, event)
is BeaconContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is BeaconContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
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)
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnPowerChanged
event: BeaconContract.Event.OnNavigateUp
) {
if(state is BeaconContract.State.Display) {
state.beacon.state.tx = event.tx
}
setEffect {
BeaconContract.Effect.HidePowerPicker
BeaconContract.Effect.Navigation.Up
}
}
@ -56,22 +91,30 @@ 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) }
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked
) {
setEffect { BeaconContract.Effect.Navigation.NavigateUp }
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnTxChanged
) {
if(state is BeaconContract.State.Display){
setState {
state.copy(
beacon = state.beacon.copy(
state = state.beacon.state.copy(
tx = event.tx
)
)
)
}
}
}
private fun reduce(
@ -94,8 +137,7 @@ class BeaconViewModel @Inject constructor(
setState {
BeaconContract.State.Display(
origin = event.ble,
beacon = bleMapper.map(event.ble) as BleView.Beacon,
writeState = null
beacon = event.ble
)
}
}
@ -107,18 +149,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnChangePassword
) {
setEffect {
BeaconContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnHideWriteBlePreview
) {
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
setEffect {
BeaconContract.Effect.HideWriteBlePreview
BeaconContract.Effect.Navigation.PasswordForm(params.bleSerial)
}
}
private fun reduce(
@ -128,83 +165,16 @@ class BeaconViewModel @Inject constructor(
if(state is BeaconContract.State.Display){
val newBle = bleViewMapper.map(state.beacon) as Ble.Beacon
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
val newBle = state.beacon
val writeRequest = Ble.Beacon.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx
)
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
BeaconContract.Effect.ShowWriteBlePreview
}
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnWriteBle
) {
if(state is BeaconContract.State.Display){
state.writeState?.let { request ->
if(request is BeaconContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is BeaconContract.State.Display) {
val newBleObject = Ble.Beacon(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
)
)
writeBle(state.beacon.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
beacon = bleMapper.map(newBleObject) as BleView.Beacon,
writeState = BeaconContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
BeaconContract.Effect.Navigation.Write(params.bleSerial, writeRequest)
}
}

View File

@ -1,164 +1,108 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.view
import androidx.compose.foundation.clickable
import androidx.compose.animation.animateContentSize
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.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.CircleShape
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.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.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.domain.model.Ble
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconViewModel
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.value
@Composable
fun DisplayState(
onEvent: (BeaconContract.Event) -> Unit,
origin: Ble.Beacon,
ble: BleView.Beacon
viewModel: BeaconViewModel,
state: BeaconContract.State.Display
) {
Column() {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = origin.info)
}
BleInfoView(
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
modifier = Modifier,
content = {
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${state.beacon.state.tx.value} db",
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(BeaconContract.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 {
onEvent(BeaconContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
) {
viewModel.setEvent(BeaconContract.Event.OnPowerEdit)
}
)
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
viewModel.setEvent(BeaconContract.Event.OnChangePassword)
}
}
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(BeaconContract.Event.OnShowWriteBlePreview)
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
Box(modifier = Modifier.fillMaxSize()) {
if(state.origin != state.beacon) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
Button(
onClick = {
viewModel.setEvent(BeaconContract.Event.OnShowWriteBlePreview)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}

View File

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

View File

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

View File

@ -0,0 +1,62 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.write
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
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.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.common.WriteFlow
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.screen.inspection.gate.table.write.BleTableWriteViewModel
import llc.arma.ble.domain.model.Ble
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun BeaconWriteScreen(
bleSerial: String,
writeRequest: Ble.Beacon.WriteRequest,
navigator: ResultBackNavigator<Boolean>
) {
val viewModel = hiltViewModel<BeaconWriteViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
WriteFlowContract.Effect.Navigation.Up ->
navigator.navigateBack()
WriteFlowContract.Effect.Navigation.UpSuccess ->
navigator.navigateBack(true)
}
}
}
Surface(
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier.padding(20.dp)
) {
WriteFlow(
state = state,
onEvent = viewModel::setEvent
)
}
}
}

View File

@ -0,0 +1,102 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.write
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.BeaconWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateWriteScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.common.WriteItemData
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
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class BeaconWriteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val writeBle: WriteBle
) : BaseViewModel<WriteFlowContract.State, WriteFlowContract.Event, WriteFlowContract.Effect>() {
init {
val params = BeaconWriteScreenDestination.argsFrom(savedStateHandle)
val items = mutableListOf<WriteItemData>()
params.writeRequest.tx?.let {
items.add(WriteItemData("Мощность", "${it.localizedName} db"))
}
setState {
WriteFlowContract.State.Display(
items
)
}
}
override fun setInitialState() = WriteFlowContract.State.Loading
override fun handleEvents(event: WriteFlowContract.Event) {
when(event){
is WriteFlowContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is WriteFlowContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnNavigateUp
){
setEffect {
when(state){
is WriteFlowContract.State.Display,
WriteFlowContract.State.Error,
WriteFlowContract.State.Loading,
WriteFlowContract.State.Writing -> WriteFlowContract.Effect.Navigation.Up
WriteFlowContract.State.Success -> WriteFlowContract.Effect.Navigation.UpSuccess
}
}
}
private var writeJob: Job? = null
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnWrite
){
val params = BeaconWriteScreenDestination.argsFrom(savedStateHandle)
setState {
WriteFlowContract.State.Writing
}
writeJob?.cancel()
writeJob = viewModelScope.launch {
writeBle(params.bleSerial, params.writeRequest).fold(
onSuccess = {
setState {
WriteFlowContract.State.Success
}
},
onFailure = {
setState {
WriteFlowContract.State.Error
}
}
)
}
}
}

View File

@ -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<BleName>,
val loadingHistoryState: List<Ble.Gate.HistoryPoint>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,527 @@
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.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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GateHistoryScreen(
bleSerial: String,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<GateHistoryViewModel>()
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<AxisPosition.Vertical.Start> { value, chartValues ->
" "
}
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { 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)
)
}
}

View File

@ -0,0 +1,151 @@
package llc.arma.ble.app.ui.screen.inspection.gate.history
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
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<GateHistoryContract.State, GateHistoryContract.Event, GateHistoryContract.Effect>() {
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<List<Ble.Gate.HistoryPoint>> -> {
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)
}
}
}

View File

@ -0,0 +1,116 @@
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
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
class GateContract {
sealed class Event : ViewEvent {
data object OnReload : Event()
data object OnWriteBle : Event()
data object OnTxSelect : Event()
data class OnPowerChanged(
val tx: Ble.BleState.TX
) : Event()
data object OnHistoryIntervalSelect : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data object OnShowReadIntervalEdit : Event()
data class OnSaveReadIntervalChanged(
val interval: Long
) : Event()
data object OnNavigateUp : Event()
data object OnChangePassword : Event()
data object OnShowHostHistory : Event()
data object OnShowHostBleTable : Event()
}
sealed class State : ViewState {
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Gate,
val gate: Ble.Gate,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data class GateWrite(
val serial: String,
val request: Ble.Gate.WriteRequest
) : Navigation()
data class ChangePassword(
val serial: String,
) : Navigation()
data object Up : Navigation()
data class GateHistory(
val ble: BleInfo,
) : Navigation()
data class BleTable(
val serial: String,
) : Navigation()
data class TxSelector(
val tx: Ble.BleState.TX?
) : Navigation()
data class ReadIntervalSelector(
val interval: Int
) : Navigation()
data class HistoryIntervalSelector(
val interval: Int
) : Navigation()
}
}
}

View File

@ -0,0 +1,195 @@
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.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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.GateWriteScreenDestination
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.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState
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.Ble
import llc.arma.ble.domain.model.BleInfo
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GateScreen(
bleSerial: String,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
writeResult: ResultRecipient<GateWriteScreenDestination, Boolean>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<GateViewModel>()
val state = viewModel.viewState.value
writeResult.onResult {
if(it) viewModel.setEvent(GateContract.Event.OnReload)
}
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){
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
))
is GateContract.Effect.Navigation.GateWrite ->
navigator.navigate(GateWriteScreenDestination(it.serial, it.request))
}
}.launchIn(this)
}
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
)
},
actions = {
if(state is GateContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnReload)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when (state) {
is GateContract.State.Display -> DisplayState(viewModel, state)
is GateContract.State.Loading -> LoadingState(viewModel, state)
}
}
}
}
@Composable
private fun LoadingState(
viewModel: GateViewModel,
state: GateContract.State.Loading,
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
RetryingLoadingTemplate(state.attempt){
viewModel.setEvent(GateContract.Event.OnNavigateUp)
}
}
}

View File

@ -0,0 +1,311 @@
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.delay
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
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 GateViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
private val writeBle: WriteBle,
) : BaseViewModel<GateContract.State, GateContract.Event, GateContract.Effect>() {
init {
loadData()
}
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.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)
is GateContract.Event.OnReload -> reduce(viewState.value, event)
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnSaveReadIntervalChanged
) {
if(state is GateContract.State.Display) {
setState {
state.copy(
gate = state.gate.copy(
gateState = state.gate.gateState.copy(
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.gateState.readInterval.toInt())
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnSaveIntervalChanged
) {
if(state is GateContract.State.Display) {
setState {
state.copy(
gate = state.gate.copy(
gateState = state.gate.gateState.copy(
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.gateState.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) {
setState {
state.copy(
gate = state.gate.copy(
state = state.gate.state.copy(
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.OnWriteBle
) {
if(state is GateContract.State.Display){
val newBle = state.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.Navigation.GateWrite(state.gate.info.serial, writeRequest)
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnReload
) {
loadData()
}
private var loadJob: Job? = null
private fun loadData(){
val params = GateScreenDestination.argsFrom(savedStateHandle)
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState {
GateContract.State.Loading(null)
}
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
GateContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
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 = ble,
writeState = null
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,178 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.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.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.value
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@Composable
fun DisplayState(
viewModel: GateViewModel,
state: GateContract.State.Display
) {
val scrollState = rememberScrollState()
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.weight(1f)
.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.gateState.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.gateState.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)
}
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.gate) {
Button(
onClick = {
viewModel.setEvent(GateContract.Event.OnWriteBle)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
}
}
}

View File

@ -0,0 +1,60 @@
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
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
class GateBleTableContract {
sealed class Event : ViewEvent {
data object OnWriteTable: Event()
data object OnRestart : Event()
data object OnSelectBle : Event()
data class OnBleSelected(
val bleSerials: List<BleName>
) : Event()
data class OnAddBle(
val ble: BleName
) : Event()
}
sealed class State : ViewState {
data class Loading(
val attempt: Int?
) : State()
data class Display(
val newTable: List<BleName>,
val savedBleTable: List<BleName>,
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data class WriteTable(
val table: List<BleName>
) : Navigation()
data class BleSelector(
val selected: List<BleName>
) : Navigation()
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,425 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.compose.animation.animateContentSize
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.material3.Scaffold
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.RemoveCircleOutline
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
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.OutlinedTextField
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.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.generated.destinations.BleSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BleTableWriteScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.domain.model.BleName
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GateBleTableScreen(
bleSerial: String,
navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<BleSelectorScreenDestination, Array<BleName>>,
writeResult: ResultRecipient<BleTableWriteScreenDestination, Boolean>
) {
val viewModel = hiltViewModel<GateBleTableViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
GateBleTableContract.Effect.Navigation.Up ->
navigator.navigateUp()
is GateBleTableContract.Effect.Navigation.BleSelector ->
navigator.navigate(BleSelectorScreenDestination(it.selected.toTypedArray()))
is GateBleTableContract.Effect.Navigation.WriteTable ->
navigator.navigate(BleTableWriteScreenDestination(bleSerial, it.table.toTypedArray()))
}
}
}
writeResult.onResult {
if(it) viewModel.setEvent(GateBleTableContract.Event.OnRestart)
}
resultRecipient.onResult {
viewModel.setEvent(GateBleTableContract.Event.OnBleSelected(it.toList()))
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = navigator::popBackStack
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
text = "Таблица BLE",
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
enabled = state is GateBleTableContract.State.Display,
onClick = {
viewModel.setEvent(GateBleTableContract.Event.OnSelectBle)
}
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = null
)
}
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when(state){
is GateBleTableContract.State.Display -> DisplayState(viewModel, state)
is GateBleTableContract.State.Loading -> LoadingState(navigator, viewModel, state)
}
}
}
}
@Composable
private fun LoadingState(
navigator: DestinationsNavigator,
viewModel: GateBleTableViewModel,
state: GateBleTableContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
RetryingLoadingTemplate(state.attempt) {
navigator.navigateUp()
}
}
}
@Composable
private fun DisplayState(
viewModel: GateBleTableViewModel,
state: GateBleTableContract.State.Display
){
var editBle by remember {
mutableStateOf<BleName?>(null)
}
Column {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
) {
val savedBleSerials = state.savedBleTable.map { it.serial }
val newBle =
state.newTable.filterNot { ble -> savedBleSerials.contains(ble.serial) }
if (newBle.isNotEmpty()) {
item {
Text(
text = "Новые BLE",
modifier = Modifier
.padding(
horizontal = 12.dp,
vertical = 8.dp
)
)
}
items(items = newBle) {
SelectBleItem(
ble = it,
onClick = {
editBle = it
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
},
shapeType = newBle.takeShapeType(it)
) {
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
}
}
}
item {
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,
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))
}
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if (state.savedBleTable.sortedBy { it.serial } != state.newTable.sortedBy { it.serial }) {
Button(
onClick = {
viewModel.setEvent(GateBleTableContract.Event.OnWriteTable)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
}
}
if (editBle != null) {
Dialog(
onDismissRequest = {
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble = editBle!!.copy()))
editBle = null
}
) {
Surface(
shape = RoundedCornerShape(24.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(24.dp)
) {
var name by remember(editBle) {
mutableStateOf(editBle?.name ?: "")
}
Text(
style = MaterialTheme.typography.titleLarge,
text = "Введите название"
)
OutlinedTextField(
value = name,
singleLine = true,
onValueChange = {
name = it
}
)
PrimaryButton(
label = "Сохранить"
) {
viewModel.setEvent(
GateBleTableContract.Event.OnAddBle(
ble = editBle!!.copy(name = name)
)
)
editBle = null
}
}
}
}
}
}
@Composable
fun SelectBleItem(
shapeType: ShapeType,
ble: BleName,
onClick: (() -> Unit)? = null,
onRemove: (() -> Unit)? = null,
){
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = shapeType.shape,
onClick = onClick ?: {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(text = ble.name)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
}
onRemove?.let {
IconButton(onClick = onRemove) {
Icon(
imageVector = Icons.Rounded.RemoveCircleOutline,
contentDescription = null
)
}
}
}
}
}
@Composable
fun SavedBleItem(
shapeType: ShapeType,
checked: Boolean,
ble: BleName,
onClick: () -> Unit
){
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = shapeType.shape,
onClick = onClick
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 16.dp)
.padding(end = 12.dp)
) {
Column {
Text(text = ble.name)
Text(text = ble.serial)
}
Checkbox(checked = checked, onCheckedChange = null)
}
}
}

View File

@ -0,0 +1,167 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.domain.model.BleName
import llc.arma.ble.domain.usecase.AddBleToHostTable
import llc.arma.ble.domain.usecase.GetBleNamesFlow
import llc.arma.ble.domain.usecase.GetFoundBle
import llc.arma.ble.domain.usecase.GetHostBleTableBySerial
import javax.inject.Inject
@HiltViewModel
class GateBleTableViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow,
private val getHostBleTableBySerial: GetHostBleTableBySerial
) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() {
init {
setEvent(GateBleTableContract.Event.OnRestart)
}
override fun setInitialState() = GateBleTableContract.State.Loading(null)
override fun handleEvents(event: GateBleTableContract.Event) {
when(event){
is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWriteTable -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnSelectBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnBleSelected -> reduce(viewState.value, event)
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnSelectBle
) {
if(state is GateBleTableContract.State.Display) {
setEffect {
GateBleTableContract.Effect.Navigation.BleSelector(
state.newTable
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnBleSelected
) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(
newTable = event.bleSerials
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWriteTable
) {
if(state is GateBleTableContract.State.Display) {
setEffect {
GateBleTableContract.Effect.Navigation.WriteTable(
state.newTable
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnAddBle
) {
if(state is GateBleTableContract.State.Display) {
if(state.newTable.any { it.serial == event.ble.serial}){
setState {
state.copy(newTable = state.newTable.filter { it.serial != event.ble.serial })
}
} else {
setState {
state.copy(newTable = state.newTable.toMutableList().apply { add(event.ble) })
}
}
}
}
private var loadJob: Job? = null
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnRestart
) {
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState {
GateBleTableContract.State.Loading(null)
}
loadJob?.cancel()
loadJob = viewModelScope.launch {
val names = getBleNamesFlow.invoke().first()
val table = retryUntilNotNull(
onNewAttempt = {
setState {
GateBleTableContract.State.Loading(it)
}
}
){
getHostBleTableBySerial(params.bleSerial).getOrNull()
}
val savedBle = table.map { ble -> BleName(
name = names.firstOrNull { it.serial == ble }?.name ?: "Безымянный",
serial = ble) }
setState {
GateBleTableContract.State.Display(
newTable = savedBle,
savedBleTable = savedBle
)
}
}
}
}

View File

@ -0,0 +1,62 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table.write
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
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.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.common.WriteFlow
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleName
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun BleTableWriteScreen(
bleSerial: String,
items: Array<BleName>,
navigator: ResultBackNavigator<Boolean>
) {
val viewModel = hiltViewModel<BleTableWriteViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
WriteFlowContract.Effect.Navigation.Up ->
navigator.navigateBack()
WriteFlowContract.Effect.Navigation.UpSuccess ->
navigator.navigateBack(true)
}
}
}
Surface(
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier.padding(20.dp)
) {
WriteFlow(
state = state,
onEvent = viewModel::setEvent
)
}
}
}

View File

@ -0,0 +1,103 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table.write
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.BleTableWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateWriteScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.common.WriteItemData
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
import llc.arma.ble.domain.usecase.AddBleToHostTable
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class BleTableWriteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val writeBle: WriteBle,
private val addBleToHostTable: AddBleToHostTable
) : BaseViewModel<WriteFlowContract.State, WriteFlowContract.Event, WriteFlowContract.Effect>() {
init {
val params = BleTableWriteScreenDestination.argsFrom(savedStateHandle)
setState {
WriteFlowContract.State.Display(
params.items.map {
WriteItemData(it.name, it.serial)
}
)
}
}
override fun setInitialState() = WriteFlowContract.State.Loading
override fun handleEvents(event: WriteFlowContract.Event) {
when(event){
is WriteFlowContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is WriteFlowContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnNavigateUp
){
setEffect {
when(state){
is WriteFlowContract.State.Display,
WriteFlowContract.State.Error,
WriteFlowContract.State.Loading,
WriteFlowContract.State.Writing -> WriteFlowContract.Effect.Navigation.Up
WriteFlowContract.State.Success -> WriteFlowContract.Effect.Navigation.UpSuccess
}
}
}
private var writeJob: Job? = null
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnWrite
){
val params = BleTableWriteScreenDestination.argsFrom(savedStateHandle)
setState {
WriteFlowContract.State.Writing
}
writeJob?.cancel()
writeJob = viewModelScope.launch {
addBleToHostTable.invoke(
serial = params.bleSerial,
ble = params.items.toList()
).fold(
onSuccess = {
setState {
WriteFlowContract.State.Success
}
},
onFailure = {
setState {
WriteFlowContract.State.Error
}
}
)
}
}
}

View File

@ -0,0 +1,62 @@
package llc.arma.ble.app.ui.screen.inspection.gate.write
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
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.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.common.WriteFlow
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.screen.inspection.gate.table.write.BleTableWriteViewModel
import llc.arma.ble.domain.model.Ble
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable
fun GateWriteScreen(
bleSerial: String,
writeRequest: Ble.Gate.WriteRequest,
navigator: ResultBackNavigator<Boolean>
) {
val viewModel = hiltViewModel<BleTableWriteViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
WriteFlowContract.Effect.Navigation.Up ->
navigator.navigateBack()
WriteFlowContract.Effect.Navigation.UpSuccess ->
navigator.navigateBack(true)
}
}
}
Surface(
shape = RoundedCornerShape(20.dp)
) {
Box(
modifier = Modifier.padding(20.dp)
) {
WriteFlow(
state = state,
onEvent = viewModel::setEvent
)
}
}
}

View File

@ -0,0 +1,113 @@
package llc.arma.ble.app.ui.screen.inspection.gate.write
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.GateWriteScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.WriteFlowContract
import llc.arma.ble.app.ui.common.WriteItemData
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
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class GateWriteViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val writeBle: WriteBle
) : BaseViewModel<WriteFlowContract.State, WriteFlowContract.Event, WriteFlowContract.Effect>() {
init {
val params = GateWriteScreenDestination.argsFrom(savedStateHandle)
val items = mutableListOf<WriteItemData>()
params.writeRequest.tx?.let {
items.add(WriteItemData("Мощность", "${it.localizedName} db"))
}
params.writeRequest.interval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
items.add(WriteItemData("Интервал измерений", "$hours ч. $minutes мин. $seconds сек."))
}
params.writeRequest.readInterval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
items.add(WriteItemData("Интервал чтения", "$hours ч. $minutes мин. $seconds сек."))
}
setState {
WriteFlowContract.State.Display(
items
)
}
}
override fun setInitialState() = WriteFlowContract.State.Loading
override fun handleEvents(event: WriteFlowContract.Event) {
when(event){
is WriteFlowContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is WriteFlowContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnNavigateUp
){
setEffect {
when(state){
is WriteFlowContract.State.Display,
WriteFlowContract.State.Error,
WriteFlowContract.State.Loading,
WriteFlowContract.State.Writing -> WriteFlowContract.Effect.Navigation.Up
WriteFlowContract.State.Success -> WriteFlowContract.Effect.Navigation.UpSuccess
}
}
}
private var writeJob: Job? = null
private fun reduce(
state: WriteFlowContract.State,
event: WriteFlowContract.Event.OnWrite
){
val params = GateWriteScreenDestination.argsFrom(savedStateHandle)
setState {
WriteFlowContract.State.Writing
}
writeJob?.cancel()
writeJob = viewModelScope.launch {
writeBle(params.bleSerial, params.writeRequest).fold(
onSuccess = {
setState {
WriteFlowContract.State.Success
}
},
onFailure = {
setState {
WriteFlowContract.State.Error
}
}
)
}
}
}

View File

@ -1,108 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host
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.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
class HostContract {
sealed class Event : ViewEvent {
data object OnWriteBle : Event()
data object OnHideWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event()
data object OnPowerEdit : Event()
data class OnBleChanged(
val ble: Ble.Host
) : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
data class OnTxChanged(val tx: Int) : Event()
data object OnShowIntervalEdit : Event()
data class OnSaveIntervalChanged(val interval: Long) : Event()
data object OnNavigateUpClicked : Event()
data object OnChangePassword : Event()
data object OnShowHostHistory : Event()
data object OnShowHostBleTable : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Display(
val origin: Ble.Host,
val host: BleView.Host,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Host.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Host.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
}
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()
sealed class Navigation : Effect() {
data object NavigateToChangePassword : Navigation()
data object NavigateUp : Navigation()
data class NavigateToHostHistory(
val ble: BleInfo,
) : Navigation()
data class NavigateToBleTable(
val serial: String,
) : Navigation()
}
}
}

View File

@ -1,157 +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.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.PowerEdit
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
}
@Composable
fun HostScreen(
ble: Ble.Host,
onNavigationEvent: (HostContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<HostViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(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
}
}
}.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) {
PowerEdit(
state = currentState.host,
onEvent = {
viewModel.setEvent(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)
}
)
}
}
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))
}
}

View File

@ -1,282 +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<HostContract.State, HostContract.Event, HostContract.Effect>() {
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)
}
}
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
)
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
)
}
}
)
}
}
}
}
}
}
}

View File

@ -1,190 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host.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.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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
onEvent: (HostContract.Event) -> Unit,
origin: Ble.Host,
ble: BleView.Host
) {
Column {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = origin.info)
}
Column(
modifier = Modifier,
content = {
BleMenuItem(
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(HostContract.Event.OnPowerEdit)
}
val hours =
ble.hostState.historyInterval / llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInHour
val minutes =
(ble.hostState.historyInterval - (hours * llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInHour)) / llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInMinute
val seconds =
(ble.hostState.historyInterval - (hours * llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInHour) - (minutes * llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInMinute)) / llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInSecond
BleMenuItem(
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек.",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(HostContract.Event.OnShowIntervalEdit)
}
BleMenuItem(
title = "График измерений",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnShowHostHistory)
}
BleMenuItem(
title = "Таблица BLE ID",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnShowHostBleTable)
}
BleMenuItem(
title = "Изменить пароль",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnChangePassword)
}
}
)
}
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(HostContract.Event.OnShowWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
}
}
}
}
@Composable
fun BleMenuItem(
title: String,
subtitle: String? = null,
icon: Painter,
onClick: () -> Unit
){
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { 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
)
}
}
Icon(
painter = icon,
contentDescription = null
)
}
}
}

View File

@ -1,619 +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.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalConfiguration
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.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.shape.LineComponent
import com.patrykandpatrick.vico.core.component.shape.Shapes.pillShape
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.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<HostHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(HostHistoryContract.Event.OnStart(ble.name, ble.serial))
}
/*DisposableEffect("ble") {
onDispose {
viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure)
}
}*/
Column() {
TopAppBar(
navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.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.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(
-0x63d850, -0x98c549, -0xc0ae4b, -0xde690d,
-0xfc560c, -0xff432c, -0xff6978, -0xb350b0,
-0x743cb6, -0x3223c7, -0x14c5, -0x3ef9,
-0x6800, -0xa8de, -0x86aab8, -0x616162,
-0x9f8275, -0xcccccd, -0xbbcca
)
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { 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 remember {
mutableStateOf(allSerials)
}
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(), 0f)
}
}
)
}
}
val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
val chart = columnChart(
innerSpacing = 2.dp,
columns = serials.map { LineComponent(color = colors[it]!!, 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 = 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,
),
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 { index, 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 { index, 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 {
object StopMeasure : Event()
data class OnStart(
val bleName: String,
val serial: String,
) : Event()
data class OnRefreshHistory(
val bleName: String,
val serial: String,
) : Event()
}
sealed class State : ViewState {
data class Display(
val bleName: String,
val bleNames: List<BleName>,
val loadingHistoryState : ProgressState<List<Ble.Host.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class HostHistoryViewModel @Inject constructor(
private val getHostHistoryBySerial: GetHostHistoryBySerial,
private val getBleBySerial: GetBleBySerial,
private val getBleNamesFlow: GetBleNamesFlow
) : BaseViewModel<HostHistoryContract.State, HostHistoryContract.Event, HostHistoryContract.Effect>() {
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)
}
}
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)
}
}
}

View File

@ -1,265 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host.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.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.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.model.BleView
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
@Composable
fun IntervalEdit(
state: BleView.Host,
onEvent: (HostContract.Event) -> Unit,
){
var value by remember(state.hostState.historyInterval) {
mutableIntStateOf((state.hostState.historyInterval).toInt())
}
val maxInterval = 10 * 24 * 60 * 60 * 1000
val minInterval = 10_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))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
HostContract.Event.OnSaveIntervalChanged(
value.toLong()
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}
const val 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
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More