diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0fc3113..8d81632 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index a1a22f4..d9d7f3b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,8 +4,11 @@ plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id("kotlin-parcelize")
+ id("androidx.room")
}
+
+
android {
namespace 'llc.arma.ble'
compileSdk 34
@@ -14,8 +17,8 @@ android {
applicationId "llc.arma.ble"
minSdk 26
targetSdk 34
- versionCode 27
- versionName "1.3.7"
+ versionCode 33
+ versionName "1.4.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -44,7 +47,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.4.3'
+ kotlinCompilerExtensionVersion '1.5.9'
}
packagingOptions {
resources {
@@ -58,11 +61,15 @@ android {
}
}
+ room {
+ schemaDirectory("$projectDir/schemas")
+ }
+
}
dependencies {
- implementation 'androidx.core:core-ktx:1.10.1'
+ 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'
@@ -82,10 +89,10 @@ dependencies {
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.navigation:navigation-compose:2.5.3'
- implementation("androidx.hilt:hilt-navigation-compose:1.1.0-alpha01")
- implementation('com.google.dagger:hilt-android:2.45')
- kapt('com.google.dagger:hilt-android-compiler:2.45')
- kapt("androidx.hilt:hilt-compiler:1.0.0")
+ 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'
diff --git a/app/src/main/java/llc/arma/ble/app/framework/di/DatabaseModule.kt b/app/src/main/java/llc/arma/ble/app/framework/di/DatabaseModule.kt
index 2a56b67..f2c1c9f 100644
--- a/app/src/main/java/llc/arma/ble/app/framework/di/DatabaseModule.kt
+++ b/app/src/main/java/llc/arma/ble/app/framework/di/DatabaseModule.kt
@@ -8,6 +8,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import llc.arma.ble.data.db.AppDatabase
+import llc.arma.ble.data.db.BleNameDao
import llc.arma.ble.data.db.RotationsDao
import javax.inject.Singleton
@@ -27,4 +28,10 @@ class DatabaseModule {
return db.getRotationsDao()
}
+ @Provides
+ @Singleton
+ fun provideBleNamesDao(db: AppDatabase): BleNameDao {
+ return db.getBleNamesDao()
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/framework/di/RepositoryBinding.kt b/app/src/main/java/llc/arma/ble/app/framework/di/RepositoryBinding.kt
index a779882..152f83f 100644
--- a/app/src/main/java/llc/arma/ble/app/framework/di/RepositoryBinding.kt
+++ b/app/src/main/java/llc/arma/ble/app/framework/di/RepositoryBinding.kt
@@ -4,10 +4,12 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+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.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
@@ -26,6 +28,9 @@ interface RepositoryBinding {
@Binds
fun bindRotationsRepository(repository: RotationsRepositoryImpl): RotationsRepository
+ @Binds
+ fun bindBleNamesRepository(repository: BleNameRepositoryImpl): BleNameRepository
+
@Binds
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository
diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt
index d1c295d..9c95a12 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt
@@ -44,6 +44,18 @@ class BleMapper @Inject constructor(
)
)
}
+
+ 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
+ )
+ )
+ }
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt
index b13ea1c..f86acbf 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt
@@ -44,6 +44,18 @@ class BleViewMapper @Inject constructor(
)
)
}
+
+ 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
+ )
+ )
+ }
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt
index 747a881..122f9ea 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt
@@ -57,6 +57,20 @@ sealed class BleView(
}
+ 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
){
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
index 5b920ed..ae71063 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt
@@ -10,6 +10,8 @@ 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.domain.model.BleInfo
@Composable
@@ -32,20 +34,12 @@ fun BleInfoView(
BleInfoItem(
icon = {
Icon(
- imageVector = when(bleInfo.type){
- BleInfo.Type.BEACON -> Icons.Rounded.Nfc
- BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
- BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
- },
+ imageVector = bleInfo.type.icon,
contentDescription = null
)
},
title = "Тип метки",
- subtitle = when(bleInfo.type){
- BleInfo.Type.BEACON -> "Маяк"
- BleInfo.Type.THERMOMETER -> "Термодатчик"
- BleInfo.Type.ACCELEROMETER -> "Акселерометр"
- }
+ subtitle = bleInfo.type.localized
)
SpecDivider()
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt
index 1119707..c200445 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListContract.kt
@@ -11,11 +11,11 @@ class BleListContract {
sealed class Event : ViewEvent {
- object OnResetFilter : Event()
+ data object OnResetFilter : Event()
- object OnHideFilter : Event()
+ data object OnHideFilter : Event()
- object OnShowFilter : Event()
+ data object OnShowFilter : Event()
data class OnConnectToBle(
val bleAddress: String
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt
index 75863aa..e8291b1 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt
@@ -122,25 +122,34 @@ fun BleListScreen(
}
)
- val filteredData = state.bleList.filter {
- (it.type == state.filter.bleType || state.filter.bleType == null) &&
- it.name.contains(state.filter.name) &&
- it.serial.contains(state.filter.mac) &&
- state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
- 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() }
- BleListContract.State.Filter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
- BleListContract.State.Filter.Field.Battery -> it.sortedBy { it.batteryLevel }
- }
- }.let {
+ 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()
+ }
+
+ 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()
+ }
- when(state.filter.sortOrder){
- BleListContract.State.Filter.Order.Asc -> it
- BleListContract.State.Filter.Order.Desc -> it.reversed()
}
}
@@ -199,7 +208,7 @@ fun BleListScreen(
}
@Composable
-private fun ItemIcon(
+fun ItemIcon(
image: @Composable BoxScope.() -> Unit
){
@@ -228,7 +237,7 @@ private fun Int.toSignalLevel(): Int {
}
@Composable
-private fun BleItem(
+fun BleItem(
ble: BleInfo,
onClick: () -> Unit
){
@@ -281,11 +290,7 @@ private fun BleItem(
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
- imageVector = when (ble.type) {
- BleInfo.Type.BEACON -> Icons.Rounded.Nfc
- BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
- BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
- },
+ imageVector = ble.type.icon,
contentDescription = null
)
}
@@ -301,7 +306,9 @@ private fun BleItem(
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.error,
- modifier = Modifier.size(12.dp).padding(2.dp)
+ modifier = Modifier
+ .size(12.dp)
+ .padding(2.dp)
) {
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt
index 1090fcc..58c1920 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/Filter.kt
@@ -16,11 +16,16 @@ 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
@@ -40,10 +45,11 @@ 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
-private val BleListContract.State.Filter.Order.localized: String
+val BleListContract.State.Filter.Order.localized: String
get() {
return when(this){
BleListContract.State.Filter.Order.Asc -> "Прямой ↓"
@@ -51,7 +57,7 @@ private val BleListContract.State.Filter.Order.localized: String
}
}
-private val BleListContract.State.Filter.Field.localized: String
+val BleListContract.State.Filter.Field.localized: String
get() {
return when(this){
BleListContract.State.Filter.Field.Name -> "Имя"
@@ -62,9 +68,10 @@ private val BleListContract.State.Filter.Field.localized: String
}
}
-private val BleInfo.Type?.localized: String
+val BleInfo.Type?.localized: String
get() {
return when(this){
+ BleInfo.Type.HOST -> "Хост"
BleInfo.Type.BEACON -> "Маяк"
BleInfo.Type.THERMOMETER -> "Термодатчик"
BleInfo.Type.ACCELEROMETER -> "Акселерометр"
@@ -72,6 +79,17 @@ private val BleInfo.Type?.localized: String
}
}
+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(
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
index 2a84526..ca775fa 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt
@@ -5,9 +5,9 @@ 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.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
+import llc.arma.ble.app.ui.screen.inspection.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
@@ -17,20 +17,23 @@ import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
-import llc.arma.ble.domain.usecase.GetBleBySerial
class ConnectionContract {
sealed class Event : ViewEvent {
- object RefreshBle : Event()
+ data object RefreshBle : Event()
- object OnNavigateUp : 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()
@@ -44,7 +47,7 @@ class ConnectionContract {
sealed class State : ViewState {
- object Loading : State()
+ data object Loading : State()
data class DisplayException(
val exception: BleException
@@ -60,7 +63,7 @@ class ConnectionContract {
sealed class Navigation : Effect() {
- object NavigateUp : Navigation()
+ data object NavigateUp : Navigation()
data class NavigateToChangePassword(
val serial: String
@@ -104,6 +107,16 @@ class ConnectionContract {
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
+ @Parcelize
+ data class NavigateToHostHistory(
+ val ble: BleInfo
+ ) : InnerNavigation(), Parcelable
+
+ @Parcelize
+ data class NavigateHostToBleTable(
+ val serial: String
+ ) : InnerNavigation(), Parcelable
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
index f86040e..88988c2 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt
@@ -3,9 +3,7 @@ package llc.arma.ble.app.ui.screen.connection
import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
-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.ArrowBack
import androidx.compose.material3.*
@@ -13,7 +11,6 @@ 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
@@ -24,19 +21,17 @@ 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.model.BleView
-import llc.arma.ble.app.ui.screen.BleInfoView
-import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerScreen
import llc.arma.ble.app.ui.screen.inspection.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.password.ChangePasswordContract
-import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
+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
-import llc.arma.ble.domain.usecase.GetBleBySerial
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
@@ -156,9 +151,22 @@ fun ConnectionScreen(
)
}
}
+
+ is Ble.Host -> {
+ HostScreen(
+ ble = state.ble,
+ onNavigationEvent = {
+ viewModel.setEvent(
+ ConnectionContract.Event.OnHostNavigationEvent(it)
+ )
+ }
+ )
+ }
+
}
}
+
}
}
@@ -217,6 +225,28 @@ fun ConnectionScreen(
)
}
+ 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
+ }
+ }
+ }
+
+ }
+
}
}
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
index 0f26681..0925cd7 100644
--- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt
@@ -7,15 +7,11 @@ 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.mapper.BleMapper
-import llc.arma.ble.app.ui.mapper.BleViewMapper
-import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
+import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
-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
@@ -33,6 +29,7 @@ class ConnectionViewModel @Inject constructor(
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)
@@ -40,6 +37,40 @@ class ConnectionViewModel @Inject constructor(
}
}
+ 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("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
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt
new file mode 100644
index 0000000..ba3b504
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostContract.kt
@@ -0,0 +1,108 @@
+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()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt
new file mode 100644
index 0000000..4a16c31
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostScreen.kt
@@ -0,0 +1,151 @@
+package llc.arma.ble.app.ui.screen.inspection.host
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
+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()
+ val state = viewModel.viewState.value
+
+ var sheetPage by rememberSaveable {
+ mutableStateOf(null)
+ }
+
+ val bottomDialog = rememberBottomDialogState()
+
+ LaunchedEffect("effect"){
+ viewModel.effect.onEach {
+ when(it){
+ is HostContract.Effect.Navigation -> onNavigationEvent(it)
+ HostContract.Effect.HideWriteBlePreview -> launch {
+ sheetPage = null
+ }
+ HostContract.Effect.ShowWriteBlePreview -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.WRITE
+ }
+ HostContract.Effect.HidePowerPicker -> launch {
+ sheetPage = null
+ }
+ HostContract.Effect.ShowPowerPicker -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.POWER_EDIT
+ }
+
+ HostContract.Effect.HideIntervalPicker -> launch {
+ sheetPage = null
+ }
+ HostContract.Effect.ShowIntervalPicker -> launch {
+ sheetPage = null
+ delay(100)
+ sheetPage = SheetPage.INTERVAL_EDIT
+ }
+ }
+ }.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))
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt
new file mode 100644
index 0000000..3b20e87
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/HostViewModel.kt
@@ -0,0 +1,282 @@
+package llc.arma.ble.app.ui.screen.inspection.host
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.app.ui.mapper.BleMapper
+import llc.arma.ble.app.ui.mapper.BleViewMapper
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.usecase.WriteBle
+import javax.inject.Inject
+
+@HiltViewModel
+class HostViewModel @Inject constructor(
+ private val bleMapper: BleMapper,
+ private val writeBle: WriteBle,
+ private val bleViewMapper: BleViewMapper
+) : BaseViewModel() {
+
+ override fun setInitialState() = HostContract.State.Loading
+
+ override fun handleEvents(event: HostContract.Event) {
+ when(event){
+ is HostContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
+ is HostContract.Event.OnTxChanged -> reduce(viewState.value, event)
+ is HostContract.Event.OnBleChanged -> reduce(viewState.value, event)
+ is HostContract.Event.OnChangePassword -> reduce(viewState.value, event)
+ is HostContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
+ is HostContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
+ is HostContract.Event.OnWriteBle -> reduce(viewState.value, event)
+ is HostContract.Event.OnPowerChanged -> reduce(viewState.value, event)
+ is HostContract.Event.OnPowerEdit -> reduce(viewState.value, event)
+ is HostContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
+ is HostContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
+ is HostContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
+ is HostContract.Event.OnShowIntervalEdit -> reduce(viewState.value, event)
+ }
+ }
+
+ 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
+ )
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt
new file mode 100644
index 0000000..8ec7251
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/DisplayState.kt
@@ -0,0 +1,180 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.KeyboardArrowRight
+import androidx.compose.material3.*
+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 = "Мощность",
+ 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
+ )
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt
new file mode 100644
index 0000000..1ea4c62
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/HostHistory.kt
@@ -0,0 +1,596 @@
+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.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.viewModelScope
+import com.patrykandpatrick.vico.compose.chart.Chart
+import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.app.ui.common.ViewEvent
+import llc.arma.ble.app.ui.common.ViewSideEffect
+import llc.arma.ble.app.ui.common.ViewState
+import llc.arma.ble.domain.model.BleInfo
+import javax.inject.Inject
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Refresh
+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 com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
+import com.patrykandpatrick.vico.compose.chart.column.columnChart
+import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
+import com.patrykandpatrick.vico.core.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.composed.ComposedChartEntryModelProducer
+import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
+import com.patrykandpatrick.vico.core.scroll.InitialScroll
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.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
+
+class HostEntry(
+ val localDate: Long,
+ override val x: Float,
+ override val y: Float,
+) : ChartEntry {
+
+ override fun withY(y: Float) = HostEntry(localDate, x, y)
+
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun HostHistory(
+ ble: BleInfo,
+ onDismiss: (() -> Unit)? = null,
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(ble.serial) {
+ viewModel.setEvent(HostHistoryContract.Event.OnStart(ble.name, ble.serial))
+ }
+
+ /*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 { 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,
+ val loadingHistoryState : ProgressState>
+ ) : 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() {
+
+ 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)
+
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt
new file mode 100644
index 0000000..8fe71fa
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/IntervalEdit.kt
@@ -0,0 +1,242 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.KeyboardArrowDown
+import androidx.compose.material.icons.rounded.KeyboardArrowUp
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.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
+ )
+ }
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/PowerEdit.kt
new file mode 100644
index 0000000..da90a6a
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/PowerEdit.kt
@@ -0,0 +1,96 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.app.ui.model.BleView
+import llc.arma.ble.app.ui.screen.inspection.host.HostContract
+
+@Composable
+fun PowerEdit(
+ state: BleView.Host,
+ onEvent: (HostContract.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(
+ HostContract.Event.OnPowerChanged(
+ value
+ )
+ )
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Применить"
+ )
+
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt
new file mode 100644
index 0000000..a8223d7
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/Write.kt
@@ -0,0 +1,388 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.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.host.HostContract
+import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
+
+@Composable
+fun Write(
+ state: HostContract.State.Display.WriteState,
+ onEvent: (HostContract.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 HostContract.State.Display.WriteState.DisplayPreview -> {
+
+ if(state.writeRequest.tx != null || state.writeRequest.interval != 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.interval?.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 / llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInHour
+ val minutes = (it - (hours * llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInHour)) / llc.arma.ble.app.ui.screen.inspection.accelerometer.view.millisInMinute
+ val seconds = (it - (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
+
+ 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(HostContract.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(HostContract.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(HostContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+
+ }
+ is HostContract.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(HostContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ HostContract.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(HostContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ HostContract.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(HostContract.Event.OnHideWriteBlePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt
new file mode 100644
index 0000000..0adfb23
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditContract.kt
@@ -0,0 +1,73 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view.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
+
+class BleTableEditContract {
+
+ sealed class Event : ViewEvent {
+
+ data object OnHideWritePreview: Event()
+
+ data object OnWritePreview: Event()
+
+ data object OnWrite: Event()
+
+
+
+ data class OnStart(
+ val serial: String
+ ) : Event()
+
+ data class OnAddBle(
+ val ble: BleInfo
+ ) : Event()
+
+ }
+
+ sealed class State : ViewState {
+
+ data object Loading : State()
+
+ data object Error : State()
+
+ data class Display(
+ val bleAround: List,
+ val newBle: List,
+ val bleTable: List,
+ val writeState: WriteState?
+ ) : State() {
+
+ sealed class WriteState {
+
+ data class DisplayPreview(
+ val writeRequest: List
+ ) : WriteState()
+
+ data class Writing(
+ val writeRequest: List
+ ) : WriteState()
+
+ data object Success : WriteState()
+
+ data object Failure : WriteState()
+
+ }
+
+ }
+
+ }
+
+ sealed class Effect : ViewSideEffect {
+
+ sealed class Navigation : Effect() {
+
+ data object NavigateUp : Navigation()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt
new file mode 100644
index 0000000..290a125
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditScreen.kt
@@ -0,0 +1,535 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view.table
+
+import androidx.compose.foundation.clickable
+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.layout.widthIn
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Add
+import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.RemoveCircleOutline
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.draw.clip
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.hilt.navigation.compose.hiltViewModel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import llc.arma.ble.app.ui.common.rememberBottomDialogState
+import llc.arma.ble.app.ui.screen.ble.BleItem
+import llc.arma.ble.app.ui.screen.ble.ItemIcon
+import llc.arma.ble.app.ui.screen.ble.icon
+import llc.arma.ble.domain.model.BleInfo
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
+@Composable
+fun BleTableEditScreen(
+ serial: String,
+ onEvent: (event: BleTableEditContract.Effect.Navigation) -> Unit
+) {
+
+ val viewModel = hiltViewModel()
+ val state = viewModel.viewState.value
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.onEach {
+ when(it){
+ is BleTableEditContract.Effect.Navigation -> onEvent(it)
+ }
+ }.launchIn(this)
+ }
+
+ LaunchedEffect(key1 = serial) {
+ viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
+ }
+
+ var showSelector by remember {
+ mutableStateOf(false)
+ }
+
+ val bottomDialog = rememberBottomDialogState()
+
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = {
+ if(showSelector){
+ showSelector = false
+ } else {
+ onEvent(BleTableEditContract.Effect.Navigation.NavigateUp)
+ }
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.ArrowBack,
+ contentDescription = null
+ )
+ }
+ },
+ title = {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = if(showSelector){
+ "Выберите BLE"
+ } else {
+ "Таблица BLE ID"
+ },
+ style = MaterialTheme.typography.titleLarge
+ )
+ },
+ actions = {
+ if(showSelector.not()){
+ IconButton(
+ enabled = state is BleTableEditContract.State.Display,
+ onClick = { showSelector=true }
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.Add,
+ contentDescription = null
+ )
+ }
+ }
+ }
+
+ )
+
+ if(state is BleTableEditContract.State.Loading){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
+
+ CircularProgressIndicator()
+
+ }
+
+ }
+
+ if(state is BleTableEditContract.State.Error){
+
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.fillMaxSize()
+ ){
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.widthIn(max = 200.dp)
+ ) {
+
+ Text(
+ textAlign = TextAlign.Center,
+ text = "Во время загрузки произошла ошибка",
+ )
+
+ Surface(
+ modifier = Modifier
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primary,
+ onClick = {
+ viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
+ }
+ ) {
+
+ Box(modifier = Modifier) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Повторить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ if (state is BleTableEditContract.State.Display) {
+
+ if(showSelector) {
+
+ BleSelectorScreen(
+ saved = state.bleTable,
+ selected = state.newBle,
+ bleList = state.bleAround,
+ onClose = {
+ showSelector = false
+ }
+ ) {
+ viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
+ }
+
+ } else {
+
+ var editBle by remember {
+ mutableStateOf(null)
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 12.dp)
+ ) {
+
+ if (state.newBle.isNotEmpty()) {
+
+ item {
+ Text(
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center,
+ text = "Новые BLE",
+ )
+ }
+
+ items(items = state.newBle) {
+ SelectBleItem(
+ ble = it,
+ onClick = {
+ editBle = it
+ viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
+ }
+ ) {
+ viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
+ }
+ }
+
+ }
+
+ if (state.bleTable.isNotEmpty()) {
+
+ item {
+ Text(
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center,
+ text = "Сохраненные BLE",
+ )
+ }
+
+ items(items = state.bleTable) {
+ SavedBleItem(it)
+ }
+
+ }
+
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ viewModel.setEvent(BleTableEditContract.Event.OnWritePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Записать"
+ )
+
+ }
+
+ }
+
+ if(editBle != null){
+
+ Dialog(
+ onDismissRequest = {
+ BleTableEditContract.Event.OnAddBle(
+ ble = editBle!!
+ )
+ 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
+ }
+ )
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ viewModel.setEvent(
+ BleTableEditContract.Event.OnAddBle(
+ ble = editBle!!.copy(name = name)
+ )
+ )
+ editBle = null
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Сохранить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+ LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) {
+ if (bottomDialog.sheetState?.isVisible?.not() == true) {
+ viewModel.setEvent(BleTableEditContract.Event.OnHideWritePreview)
+ }
+ }
+
+ LaunchedEffect(key1 = state.writeState) {
+
+ if (state.writeState == null) {
+ bottomDialog.hide()
+ } else {
+ bottomDialog.show {
+ Write(
+ state = state.writeState,
+ onEvent = {
+ viewModel.setEvent(it)
+ }
+ )
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+
+}
+
+@Composable
+fun BleSelectorScreen(
+ saved: List,
+ selected: List,
+ bleList: List,
+ onClose: () -> Unit,
+ onAddBle: (BleInfo) -> Unit
+) {
+
+ Column(
+ modifier = Modifier.fillMaxSize()
+ ) {
+
+ LazyColumn(
+ modifier = Modifier.weight(1f)
+ ) {
+
+ items(items = bleList.filterNot { saved.contains(it.serial) }) { ble ->
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .clickable {
+ onAddBle(ble)
+ }
+ ) {
+
+ Checkbox(
+ checked = selected.any { it.serial == ble.serial },
+ onCheckedChange = null
+ )
+
+ BleItem(ble = ble) {
+ onAddBle(ble)
+ }
+
+ }
+
+ }
+
+
+ }
+
+ Surface(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ .height(50.dp),
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onClose()
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.background,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Сохранить"
+ )
+
+ }
+
+ }
+
+ }
+
+}
+
+@Composable
+fun SelectBleItem(
+ ble: BleInfo,
+ onClick: (() -> Unit)? = null,
+ onRemove: (() -> Unit)? = null,
+){
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ .clickable { onClick?.invoke() }
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+
+ ) {
+
+ Box {
+
+ ItemIcon {
+ Icon(
+ modifier = Modifier.align(Alignment.Center),
+ imageVector = ble.type.icon,
+ contentDescription = null
+ )
+ }
+
+ }
+
+ 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(
+ serial: String
+){
+
+ Box {
+
+ Text(
+ text = serial,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ )
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt
new file mode 100644
index 0000000..5ef4925
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/BleTableEditViewModel.kt
@@ -0,0 +1,187 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view.table
+
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import llc.arma.ble.app.ui.common.BaseViewModel
+import llc.arma.ble.domain.usecase.AddBleToHostTable
+import llc.arma.ble.domain.usecase.GetFoundBle
+import llc.arma.ble.domain.usecase.GetHostBleTableBySerial
+import javax.inject.Inject
+
+@HiltViewModel
+class BleTableEditViewModel @Inject constructor(
+ getFoundBle: GetFoundBle,
+ private val addBleToHostTable: AddBleToHostTable,
+ private val getHostBleTableBySerial: GetHostBleTableBySerial
+) : BaseViewModel() {
+
+ private var lastSerial: String = ""
+
+ init {
+
+ viewModelScope.launch {
+
+ while (true){
+
+ val state = viewState.value
+
+ if(state is BleTableEditContract.State.Display) {
+
+ setState {
+ state.copy(bleAround = getFoundBle())
+ }
+
+ }
+ delay(1_000)
+
+
+ }
+
+ }
+
+ }
+
+ override fun setInitialState() = BleTableEditContract.State.Loading
+
+ override fun handleEvents(event: BleTableEditContract.Event) {
+ when(event){
+ is BleTableEditContract.Event.OnStart -> reduce(viewState.value, event)
+ is BleTableEditContract.Event.OnAddBle -> reduce(viewState.value, event)
+ is BleTableEditContract.Event.OnWritePreview -> reduce(viewState.value, event)
+ is BleTableEditContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
+ is BleTableEditContract.Event.OnWrite -> reduce(viewState.value, event)
+ }
+ }
+
+ private fun reduce(
+ state: BleTableEditContract.State,
+ event: BleTableEditContract.Event.OnWrite
+ ) {
+
+ if(state is BleTableEditContract.State.Display) {
+
+ viewModelScope.launch {
+
+ setState {
+ state.copy(
+ writeState = BleTableEditContract.State.Display.WriteState.Writing(state.newBle)
+ )
+ }
+
+ addBleToHostTable.invoke(
+ serial = lastSerial,
+ ble = state.newBle
+ ).fold(
+ onSuccess = {
+ setState {
+ state.copy(
+ writeState = BleTableEditContract.State.Display.WriteState.Success
+ )
+ }
+ setEvent(BleTableEditContract.Event.OnStart(lastSerial))
+ },
+ onFailure = {
+ setState {
+ state.copy(
+ writeState = BleTableEditContract.State.Display.WriteState.Failure
+ )
+ }
+ }
+ )
+
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BleTableEditContract.State,
+ event: BleTableEditContract.Event.OnHideWritePreview
+ ) {
+
+ if(state is BleTableEditContract.State.Display) {
+
+ setState {
+ state.copy(writeState = null)
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BleTableEditContract.State,
+ event: BleTableEditContract.Event.OnWritePreview
+ ) {
+
+ if(state is BleTableEditContract.State.Display) {
+
+ setState {
+ state.copy(writeState = BleTableEditContract.State.Display.WriteState.DisplayPreview(state.newBle))
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BleTableEditContract.State,
+ event: BleTableEditContract.Event.OnAddBle
+ ) {
+
+ if(state is BleTableEditContract.State.Display) {
+
+ if(state.newBle.any { it.serial == event.ble.serial}){
+
+ setState {
+ state.copy(newBle = state.newBle.filter { it.serial != event.ble.serial})
+ }
+
+ } else {
+
+ setState {
+ state.copy(newBle = state.newBle.toMutableList().apply { add(event.ble) })
+ }
+
+ }
+
+ }
+
+ }
+
+ private fun reduce(
+ state: BleTableEditContract.State,
+ event: BleTableEditContract.Event.OnStart
+ ) {
+
+ lastSerial = event.serial
+
+ setState {
+ BleTableEditContract.State.Loading
+ }
+
+ viewModelScope.launch {
+
+ getHostBleTableBySerial(event.serial).fold(
+ onSuccess = {
+
+ setState {
+ BleTableEditContract.State.Display(emptyList(), emptyList(), it, null)
+ }
+
+ },
+ onFailure = {
+ setState {
+ BleTableEditContract.State.Error
+ }
+ }
+ )
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt
new file mode 100644
index 0000000..8408279
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/app/ui/screen/inspection/host/view/table/Write.kt
@@ -0,0 +1,350 @@
+package llc.arma.ble.app.ui.screen.inspection.host.view.table
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+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.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import llc.arma.ble.R
+
+@Composable
+fun Write(
+ state: BleTableEditContract.State.Display.WriteState,
+ onEvent: (BleTableEditContract.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 BleTableEditContract.State.Display.WriteState.DisplayPreview -> {
+
+ if(state.writeRequest.isNotEmpty()) {
+
+ Box(
+ modifier = Modifier.padding(
+ vertical = 0.dp,
+ horizontal = 8.dp
+ )
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .padding(8.dp)
+ ) {
+
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 12.dp)
+ ) {
+
+ item {
+ Text(
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center,
+ text = "Новые BLE",
+ )
+ }
+
+ items(items = state.writeRequest){
+ SelectBleItem(it)
+ }
+
+ }
+
+ }
+
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ onClick = {
+ onEvent(BleTableEditContract.Event.OnWrite)
+ },
+ 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(BleTableEditContract.Event.OnHideWritePreview)
+ },
+ 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(BleTableEditContract.Event.OnHideWritePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+
+ }
+ is BleTableEditContract.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(BleTableEditContract.Event.OnHideWritePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Отменить"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ BleTableEditContract.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(BleTableEditContract.Event.OnHideWritePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ BleTableEditContract.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(BleTableEditContract.Event.OnHideWritePreview)
+ }
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+
+ Text(
+ modifier = Modifier.align(Alignment.Center),
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.labelLarge,
+ text = "Ок"
+ )
+
+ }
+
+ }
+
+ }
+
+ }
+
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/db/AppDatabase.kt b/app/src/main/java/llc/arma/ble/data/db/AppDatabase.kt
index 4b4d947..e821310 100644
--- a/app/src/main/java/llc/arma/ble/data/db/AppDatabase.kt
+++ b/app/src/main/java/llc/arma/ble/data/db/AppDatabase.kt
@@ -1,16 +1,23 @@
package llc.arma.ble.data.db
+import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
+import llc.arma.ble.data.model.BleNameEntity
import llc.arma.ble.data.model.RotationEntity
import llc.arma.ble.data.model.WheelEntity
@Database(
- entities = [RotationEntity::class, WheelEntity::class],
- version = 1
+ entities = [RotationEntity::class, WheelEntity::class, BleNameEntity::class],
+ version = 2,
+ autoMigrations = [
+ AutoMigration (from = 1, to = 2)
+ ]
)
abstract class AppDatabase : RoomDatabase() {
abstract fun getRotationsDao(): RotationsDao
+ abstract fun getBleNamesDao(): BleNameDao
+
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/db/BleNameDao.kt b/app/src/main/java/llc/arma/ble/data/db/BleNameDao.kt
new file mode 100644
index 0000000..c7ab3eb
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/data/db/BleNameDao.kt
@@ -0,0 +1,19 @@
+package llc.arma.ble.data.db
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import llc.arma.ble.data.model.BleNameEntity
+
+@Dao
+interface BleNameDao {
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun save(names: List)
+
+ @Query("select * from ble_name")
+ fun getAllFlow(): Flow>
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/model/BleNameEntity.kt b/app/src/main/java/llc/arma/ble/data/model/BleNameEntity.kt
new file mode 100644
index 0000000..4e3adef
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/data/model/BleNameEntity.kt
@@ -0,0 +1,16 @@
+package llc.arma.ble.data.model
+
+import androidx.room.Entity
+import androidx.room.Index
+import androidx.room.PrimaryKey
+import llc.arma.ble.domain.usecase.AccelScale
+import llc.arma.ble.domain.usecase.AccelViewMode
+
+@Entity(
+ tableName = "ble_name",
+ indices = [Index(unique = true, value = ["serial"])],)
+class BleNameEntity(
+ @PrimaryKey
+ val serial: String,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/repository/BleNameRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/repository/BleNameRepositoryImpl.kt
new file mode 100644
index 0000000..48bda0f
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/data/repository/BleNameRepositoryImpl.kt
@@ -0,0 +1,37 @@
+package llc.arma.ble.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import llc.arma.ble.data.db.BleNameDao
+import llc.arma.ble.data.model.BleNameEntity
+import llc.arma.ble.domain.model.BleName
+import llc.arma.ble.domain.repository.BleNameRepository
+import javax.inject.Inject
+
+class BleNameRepositoryImpl @Inject constructor(
+ private val bleNameDao: BleNameDao
+) : BleNameRepository {
+
+ override suspend fun save(names: List) {
+ bleNameDao.save(
+ names.map {
+ BleNameEntity(
+ serial = it.serial,
+ name = it.name
+ )
+ }
+ )
+ }
+
+ override fun getNamesFlow(): Flow> {
+ return bleNameDao.getAllFlow().map { list ->
+ list.map {
+ BleName(
+ serial = it.serial,
+ name = it.name
+ )
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
index 32e40d9..e97e77d 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/BleRepositoryImpl.kt
@@ -20,6 +20,7 @@ import llc.arma.ble.data.repository.extensions.fromByte
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.repository.extensions.info
import llc.arma.ble.data.repository.extensions.sendData
+import llc.arma.ble.data.repository.extensions.to4ByteArrayInLittleEndian
import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
@@ -53,6 +54,7 @@ import kotlin.math.atan
val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb")
+val hostHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val accelerometerHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
@@ -74,6 +76,10 @@ class BleRepositoryImpl @Inject constructor(
val resultList: MutableMap = Collections.synchronizedMap(mutableMapOf())
+ override fun getFoundBle(): List {
+ return resultList.values.toList()
+ }
+
override fun getBleAroundFlow(): Result>, BleException> {
return if(app.checkPermission()){
@@ -269,6 +275,55 @@ class BleRepositoryImpl @Inject constructor(
)
}
+
+ BleInfo.Type.HOST -> {
+
+ val tState = readHostState(result.serial).fold(
+ onFailure = {
+ return Result.failure(it)
+ },
+ onSuccess = {
+ it
+ }
+ )
+
+ Result.success(
+ flow {
+
+ while (true) {
+
+ resultList[serial]?.let { newResult ->
+
+ val state = Ble.BleState(
+ tx = Ble.BleState.TX.fromByte(result.tx.toByte())
+ ?: Ble.BleState.TX.ZERO
+ )
+
+ emit(
+ Ble.Host(
+ info = newResult.copy(
+ rssi = if((SystemClock.elapsedRealtime() - newResult.scanTime) > 15_000) {
+ null
+ } else {
+ newResult.rssi
+ }
+
+ ),
+ state = state,
+ hostState = tState
+ )
+ )
+
+ }
+
+ delay(1_000)
+
+ }
+
+ }
+ )
+
+ }
}
}
@@ -340,6 +395,56 @@ class BleRepositoryImpl @Inject constructor(
}
+ private suspend fun readHostState(
+ address: String
+ ): Result {
+
+ return if(app.checkPermission()) {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.IO))
+
+ try {
+
+ val service = connection.discoverServices()
+ .findService(serviceUUID) ?: return Result.failure(BleException.UnexpectedResponse)
+
+ val characteristic = service.findCharacteristic(intervalReadUUID)
+ ?: return Result.failure(BleException.UnexpectedResponse)
+
+ characteristic.write(DataByteArray.from(3, 0, 0, 0, ))
+
+ val interval = characteristic.read().value.let {
+ if(it.size == 4){
+ it.get4byteUIntAt(0).toLong()
+ }else{
+ 0
+ }
+ }
+
+ return Result.success(
+ Ble.Host.HostState(
+ historyInterval = interval
+ )
+ )
+
+ } catch (err: Throwable){
+ err.printStackTrace()
+ return Result.failure(BleException.UnexpectedResponse)
+ } finally {
+
+ connection.close()
+
+ }
+
+ } else {
+
+ Result.failure(BleException.PermissionDenied)
+
+ }
+
+ }
+
private suspend fun readAccelState(
address: String,
timer: Boolean
@@ -440,6 +545,24 @@ class BleRepositoryImpl @Inject constructor(
}
+ override suspend fun getHostBleTableBySerial(
+ serial: String
+ ): Result, BleException> {
+ return readHostBleTable(serial, app)
+ }
+
+ override suspend fun addBleToHostTableBySerial(serial: String, ble: List): Result {
+ return addBleToHostTable(serial, ble, app)
+ }
+
+ override suspend fun getHostHistoryBySerial(
+ serial: String
+ ): Flow>, BleException>> {
+
+ return readHostHistory(serial, app)
+
+ }
+
override suspend fun getAccelerometerHistoryBySerial(
serial: String
): Flow>, BleException>> {
@@ -577,13 +700,75 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun writeBle(
serial: String,
- request: Ble.Accelerometer.WriteRequest
+ request: Ble.Host.WriteRequest
): Result {
- fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
- (3 downTo 0).map {
- (this shr (it * Byte.SIZE_BITS)).toByte()
- }.toByteArray()
+ return if(app.checkPermission()) {
+
+ val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val services = connection.discoverServices()
+ val service = services.findService(serviceUUID) ?: return Result.failure(
+ BleException.UnexpectedResponse
+ )
+
+ request.tx?.let {
+
+ service.findCharacteristic(
+ txWriteUUID
+ )?.write(
+ DataByteArray.from(it.sendData)
+ ) ?: return Result.failure(BleException.UnexpectedResponse)
+
+ }
+
+ request.interval?.let {
+
+ service.findCharacteristic(intervalWriteUUID)!!.write(
+ DataByteArray.from(
+ *mutableListOf(3).apply {
+ addAll(
+ (it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
+ )
+ }.toByteArray()
+ )
+ )
+
+ }
+
+ connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
+ flashWriteUUID
+ )!!.write(
+ DataByteArray.from(9)
+ )
+
+ Result.success(Unit)
+
+ } catch (err: Throwable){
+
+ err.printStackTrace()
+ Result.failure(BleException.UnexpectedResponse)
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ } else {
+
+ Result.failure(BleException.PermissionDenied)
+
+ }
+
+ }
+
+ override suspend fun writeBle(
+ serial: String,
+ request: Ble.Accelerometer.WriteRequest
+ ): Result {
rotationsDao.deleteBySerial(serial)
diff --git a/app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectreCallback.kt b/app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectre.kt
similarity index 99%
rename from app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectreCallback.kt
rename to app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectre.kt
index f0def63..0e2ac31 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectreCallback.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/ReadAccelerometerSpectre.kt
@@ -24,8 +24,6 @@ import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import java.util.UUID
-
-
fun readAccelerometerSpectre(
address: String,
app: Application,
diff --git a/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt b/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt
new file mode 100644
index 0000000..ae66b20
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/data/repository/ReadHostHistory.kt
@@ -0,0 +1,412 @@
+package llc.arma.ble.data.repository
+
+import android.app.Application
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import llc.arma.ble.data.repository.extensions.checkPermission
+import llc.arma.ble.data.repository.extensions.get2byteUIntAt
+import llc.arma.ble.data.repository.extensions.get4byteUIntAt
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import no.nordicsemi.android.common.core.DataByteArray
+import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
+import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
+import java.nio.ByteBuffer
+import java.util.BitSet
+import java.util.Locale
+
+suspend fun readTable(
+ characteristic: ClientBleGattCharacteristic,
+ startRequest: ByteArray,
+ nextRequestPayload: ByteArray
+): List {
+
+ characteristic.write(DataByteArray(startRequest))
+ var value = characteristic.read().value
+ var nextPackageDataCount = value.get2byteUIntAt(2)
+
+ val tableResult = mutableListOf()
+
+ do {
+
+ nextPackageDataCount = value.get2byteUIntAt(2)
+
+ tableResult.addAll(value.asList().subList(4, value.size))
+
+ characteristic.write(DataByteArray(nextRequestPayload))
+ value = characteristic.read().value
+
+ } while (nextPackageDataCount.toInt() != 0)
+
+ return tableResult
+
+}
+
+@OptIn(ExperimentalStdlibApi::class)
+fun readHostHistory(
+ address: String,
+ app: Application,
+): Flow>, BleException>> {
+
+ return flow {
+
+ if (app.checkPermission()) {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(hostHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(DataByteArray.from(2))
+
+ var value = characteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ emit(Result.success(ProgressState.Finished(emptyList())))
+
+ } else {
+
+ val firstTablePackage: MutableList = mutableListOf()
+ val secondTablePackage: MutableList = mutableListOf()
+
+ var tableSize = value.get2byteUIntAt(0)
+
+ firstTablePackage.addAll(
+ readTable(
+ characteristic,
+ mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray(),
+ byteArrayOf(5)
+ )
+ )
+
+ val bleMeasureInterval = firstTablePackage.toByteArray().get4byteUIntAt(0).toLong()
+ val bleLastMeasureTime = firstTablePackage.toByteArray().get4byteUIntAt(4).toLong()
+ val bleRealTime = firstTablePackage.toByteArray().get4byteUIntAt(8).toLong()
+ val lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
+
+ secondTablePackage.addAll(
+ readTable(characteristic, byteArrayOf(6), byteArrayOf(6))
+ )
+
+ fun getBleIdIndex(bytes: ByteArray): UInt{
+
+ val bits = BitSet.valueOf(bytes)
+ bits.clear(12, 16)
+
+ val arr = bits.toByteArray()
+
+ if(arr.isEmpty()){
+ return 0x00.toUInt()
+ }
+
+ if(arr.size == 1){
+ return arr[0].toUInt()
+ }
+
+ return arr.get2byteUIntAt(0)
+
+ }
+
+ fun getInnerIndex(byte: Byte): Int{
+
+ if(byte != 0.toByte()){
+ println(byte)
+ }
+
+ var bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(0, 4)
+ bits = bits.get(4, 8)
+ val arr = bits.toByteArray()
+
+ if(arr.isEmpty()){
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ fun getDevType(byte: Byte): Int{
+
+ var bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(5, 8)
+ val arr = bits.toByteArray()
+
+ if(arr.isEmpty()){
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ fun getDevDataSize(byte: Byte): Int{
+
+ var bits = BitSet.valueOf(byteArrayOf(byte))
+ bits.clear(0, 5)
+ bits = bits.get(4, 8)
+ val arr = bits.toByteArray()
+
+ if(arr.isEmpty()){
+ return 0x00
+ }
+
+ return bits.toByteArray()[0].toInt()
+
+ }
+
+ var bleTableOffset = 12
+ var periods = mutableListOf>()
+ var periodBle = mutableListOf()
+
+
+ do {
+
+ val bleIdTableCell = firstTablePackage.drop(bleTableOffset).take(2).toByteArray()
+
+ if(bleIdTableCell.contentEquals(byteArrayOf(-1, 15)).not()) {
+
+ println("offset $bleTableOffset/${firstTablePackage.size}")
+
+ val innerIndex = getInnerIndex(bleIdTableCell[1])
+
+ println("inner index $innerIndex")
+
+ val bleTableIndex = getBleIdIndex(bleIdTableCell) * 8u
+
+ println("table index $bleTableIndex")
+
+ val serial =
+ secondTablePackage.drop(bleTableIndex.toInt()).take(6).reversed()
+ .joinToString(
+ separator = ":",
+ transform = { it.toHexString().padStart(2, '0') })
+ .uppercase(Locale.getDefault())
+ val devTypeByte = secondTablePackage.drop(bleTableIndex.toInt() + 6)[0]
+
+ println("table serial $serial")
+
+ val devType = getDevType(devTypeByte)
+ val devDataSize = getDevDataSize(devTypeByte)
+
+ bleTableOffset += 2
+
+ if (devDataSize != 0) {
+ val payload = getBleIdIndex(
+ firstTablePackage.drop(bleTableOffset).take(devDataSize)
+ .toByteArray()
+ )
+ bleTableOffset += devDataSize
+ }
+
+ periodBle.add(serial)
+
+ } else {
+
+ bleTableOffset += 2
+
+ }
+
+
+
+ var nextIndex = 0
+
+ if(bleTableOffset <= firstTablePackage.size - 2){
+ nextIndex = getInnerIndex(firstTablePackage.drop(bleTableOffset)[1])
+ }
+
+ if(nextIndex == 0){
+ println("________________")
+ periods.add(periodBle)
+ periodBle = mutableListOf()
+ }
+
+ } while (bleTableOffset < firstTablePackage.size)
+
+ //periods.add(periodBle)
+
+ emit(
+ Result.success(
+ ProgressState.Finished(
+ periods.withIndex().map {
+ Ble.Host.HistoryPoint(
+ date = lastMeasureSystemTime - (((periods.size - 1) - it.index) * bleMeasureInterval),
+ value = it.value
+ )
+ }
+ )
+ )
+
+ )
+
+ }
+
+
+ } catch (err: Throwable) {
+ err.printStackTrace()
+ emit(Result.failure(BleException.UnexpectedResponse))
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ } else {
+
+ emit(Result.failure(BleException.PermissionDenied))
+
+ }
+
+ }
+
+}
+
+@OptIn(ExperimentalStdlibApi::class)
+suspend fun readHostBleTable(
+ address: String,
+ app: Application,
+): Result, BleException> {
+
+ return if (app.checkPermission()) {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(hostHistoryReadUUID)
+ ?: throw IllegalStateException()
+
+ characteristic.write(DataByteArray.from(2))
+
+ var value = characteristic.read().value
+
+ if (value.contentEquals(byteArrayOf(0, 0))) {
+
+ Result.success(emptyList())
+
+ } else {
+
+ var tableSize = value.get2byteUIntAt(0)
+
+ val writeData = mutableListOf(
+ 1.toByte(),
+ 0.toByte(),
+ 0.toByte()
+ ).apply {
+ addAll(value.toList())
+ }.toByteArray()
+
+ characteristic.write(DataByteArray(writeData))
+ value = characteristic.read().value
+
+ Result.success(
+ readTable(characteristic, byteArrayOf(6), byteArrayOf(6)).chunked(8).map {
+ it.take(6)
+ .reversed()
+ .joinToString(
+ separator = ":",
+ transform = { it.toHexString().padStart(2, '0') })
+ .uppercase(Locale.getDefault())
+ }
+ )
+
+ }
+
+
+ } catch (err: Throwable) {
+
+ Result.failure(BleException.UnexpectedResponse)
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ } else {
+
+ Result.failure(BleException.PermissionDenied)
+
+ }
+
+}
+
+@OptIn(ExperimentalStdlibApi::class)
+suspend fun addBleToHostTable(
+ address: String,
+ newBleAddress: List,
+ app: Application,
+): Result {
+
+ return if (app.checkPermission()) {
+
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+
+ try {
+
+ val characteristic = connection.discoverServices()
+ .findService(serviceUUID)
+ ?.findCharacteristic(flashWriteUUID)
+ ?: throw IllegalStateException()
+
+ val writeCount = newBleAddress.chunked(40).sumOf { bleAddressBatch ->
+
+ val countPayload = ByteBuffer.allocate(2).putShort(bleAddressBatch.size.toShort()).array().reversed().toByteArray()
+
+ val command = "0b00".hexToByteArray()
+
+ val serialPayload = bleAddressBatch.flatMap {
+ it.replace(":", "").lowercase(Locale.CANADA).hexToByteArray().reversed().toList()
+ }.toByteArray()
+
+ characteristic.write(DataByteArray.from(*command, *countPayload, *serialPayload))
+ characteristic.read().value.get2byteUIntAt(0).toInt()
+
+ }
+
+ characteristic.write(
+ DataByteArray.from(9)
+ )
+
+ Result.success(writeCount)
+
+ } catch (err: Throwable) {
+
+ err.printStackTrace()
+
+ Result.failure(BleException.UnexpectedResponse)
+
+ } finally {
+
+ connection.close()
+
+ }
+
+ } else {
+
+ Result.failure(BleException.PermissionDenied)
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistoryCallback.kt b/app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistory.kt
similarity index 96%
rename from app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistoryCallback.kt
rename to app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistory.kt
index d50a0b8..5735d92 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistoryCallback.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/ReadTemperatureHistory.kt
@@ -36,10 +36,10 @@ fun readThermometerHistory(
if (app.checkPermission()) {
- try {
+ val connection =
+ ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
- val connection =
- ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
+ try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
@@ -56,6 +56,8 @@ fun readThermometerHistory(
} else {
+ var nextPackageDataCount = value.get2byteUIntAt(2)
+
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
@@ -66,7 +68,6 @@ fun readThermometerHistory(
characteristic.write(DataByteArray(writeData))
value = characteristic.read().value
- var nextPackageDataCount = value.get2byteUIntAt(2)
while (nextPackageDataCount.toInt() != 0) {
@@ -127,6 +128,10 @@ fun readThermometerHistory(
emit(Result.failure(BleException.UnexpectedResponse))
+ } finally {
+
+ connection.close()
+
}
} else {
diff --git a/app/src/main/java/llc/arma/ble/data/repository/extensions/BleScanResultExtensions.kt b/app/src/main/java/llc/arma/ble/data/repository/extensions/BleScanResultExtensions.kt
index 35fdf1e..3d28dab 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/extensions/BleScanResultExtensions.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/extensions/BleScanResultExtensions.kt
@@ -32,6 +32,7 @@ val BleScanResult.batteryLevel: Int?
val BleScanResult.type: BleInfo.Type
get() {
return when(data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(0)?.toUByte()?.toInt()){
+ 4 -> BleInfo.Type.HOST
1 -> BleInfo.Type.BEACON
2 -> BleInfo.Type.THERMOMETER
else -> BleInfo.Type.ACCELEROMETER
diff --git a/app/src/main/java/llc/arma/ble/data/repository/extensions/ByteArrayExtensions.kt b/app/src/main/java/llc/arma/ble/data/repository/extensions/ByteArrayExtensions.kt
index 434c076..535617f 100644
--- a/app/src/main/java/llc/arma/ble/data/repository/extensions/ByteArrayExtensions.kt
+++ b/app/src/main/java/llc/arma/ble/data/repository/extensions/ByteArrayExtensions.kt
@@ -19,6 +19,11 @@ fun ByteArray.get2byteUIntAt(idx: Int) =
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
+fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
+ (3 downTo 0).map {
+ (this shr (it * Byte.SIZE_BITS)).toByte()
+ }.toByteArray()
+
@OptIn(ExperimentalUnsignedTypes::class)
fun UByteArray.toTemperature(): Float {
diff --git a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt
index abd3912..22a1ed4 100644
--- a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt
+++ b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt
@@ -21,7 +21,7 @@ sealed class Ble(
val detailed: Boolean
) : HistorySettings()
- object Disabled : HistorySettings()
+ data object Disabled : HistorySettings()
}
@@ -108,6 +108,28 @@ sealed class Ble(
}
+ class Host(
+ info: BleInfo,
+ val state: BleState,
+ val hostState: HostState
+ ) : Ble(info){
+
+ class HistoryPoint(
+ val date: Long,
+ val value: List
+ )
+
+ data class HostState(
+ val historyInterval: Long
+ )
+
+ data class WriteRequest(
+ val tx: BleState.TX?,
+ val interval: Long?
+ )
+
+ }
+
class Thermometer(
info: BleInfo,
val state: BleState,
diff --git a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt
index 175b911..ed47373 100644
--- a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt
+++ b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt
@@ -16,7 +16,7 @@ data class BleInfo(
) : Parcelable {
enum class Type {
- BEACON, THERMOMETER, ACCELEROMETER
+ HOST, BEACON, THERMOMETER, ACCELEROMETER
}
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/model/BleName.kt b/app/src/main/java/llc/arma/ble/domain/model/BleName.kt
new file mode 100644
index 0000000..fcbe9b6
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/model/BleName.kt
@@ -0,0 +1,9 @@
+package llc.arma.ble.domain.model
+
+import llc.arma.ble.domain.usecase.AccelScale
+import llc.arma.ble.domain.usecase.AccelViewMode
+
+data class BleName(
+ val serial: String,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleNameRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleNameRepository.kt
new file mode 100644
index 0000000..70bc593
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/repository/BleNameRepository.kt
@@ -0,0 +1,12 @@
+package llc.arma.ble.domain.repository
+
+import kotlinx.coroutines.flow.Flow
+import llc.arma.ble.domain.model.BleName
+
+interface BleNameRepository {
+
+ suspend fun save(names: List)
+
+ fun getNamesFlow(): Flow>
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
index 9c3c3dd..6a35b95 100644
--- a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
+++ b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt
@@ -6,7 +6,6 @@ import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
-import llc.arma.ble.domain.model.ConnectedBleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
@@ -15,19 +14,40 @@ import llc.arma.ble.domain.usecase.FftViewMode
interface BleRepository {
+ fun getFoundBle(): List
+
fun getBleAroundFlow(): Result>, BleException>
suspend fun getBleBySerial(serial: String) : Result, BleException>
- suspend fun getTemperatureHistoryBySerial(serial: String): Flow>, BleException>>
+ suspend fun getTemperatureHistoryBySerial(
+ serial: String
+ ): Flow>, BleException>>
- suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest): Result
+ suspend fun writeBle(
+ serial: String,
+ request: Ble.Thermometer.WriteRequest
+ ): Result
- suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result
+ suspend fun writeBle(
+ serial: String,
+ request: Ble.Beacon.WriteRequest
+ ): Result
- suspend fun writeBle(serial: String, request: Ble.Accelerometer.WriteRequest): Result
+ suspend fun writeBle(
+ serial: String,
+ request: Ble.Host.WriteRequest
+ ): Result
- suspend fun changeBlePassword(password: String, serial: String): Result
+ suspend fun writeBle(
+ serial: String,
+ request: Ble.Accelerometer.WriteRequest
+ ): Result
+
+ suspend fun changeBlePassword(
+ password: String,
+ serial: String
+ ): Result
fun getAccelerometerMeasureBySerialFlow(
serial: String,
@@ -47,6 +67,21 @@ interface BleRepository {
frequency: FftFrequency
): Flow>, BleException>>
- suspend fun getAccelerometerHistoryBySerial(serial: String): Flow>, BleException>>
+ suspend fun getAccelerometerHistoryBySerial(
+ serial: String
+ ): Flow>, BleException>>
+
+ suspend fun getHostHistoryBySerial(
+ serial: String
+ ): Flow>, BleException>>
+
+ suspend fun getHostBleTableBySerial(
+ serial: String
+ ): Result, BleException>
+
+ suspend fun addBleToHostTableBySerial(
+ serial: String,
+ ble: List
+ ): Result
}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/AddBleToHostTable.kt b/app/src/main/java/llc/arma/ble/domain/usecase/AddBleToHostTable.kt
new file mode 100644
index 0000000..5d37a4d
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/AddBleToHostTable.kt
@@ -0,0 +1,31 @@
+package llc.arma.ble.domain.usecase
+
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.model.BleInfo
+import llc.arma.ble.domain.model.BleName
+import llc.arma.ble.domain.repository.BleNameRepository
+import llc.arma.ble.domain.repository.BleRepository
+import javax.inject.Inject
+
+class AddBleToHostTable @Inject constructor(
+ private val bleRepository: BleRepository,
+ private val bleNameRepository: BleNameRepository
+) {
+
+ suspend operator fun invoke(serial: String, ble: List): Result {
+
+ bleNameRepository.save(
+ ble.map {
+ BleName(
+ serial = it.serial,
+ name = it.name
+ )
+ }
+ )
+
+ return bleRepository.addBleToHostTableBySerial(serial, ble.map { it.serial })
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetBleNamesFlow.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleNamesFlow.kt
new file mode 100644
index 0000000..7b1fb9a
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetBleNamesFlow.kt
@@ -0,0 +1,18 @@
+package llc.arma.ble.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import llc.arma.ble.domain.model.BleName
+import llc.arma.ble.domain.repository.BleNameRepository
+import javax.inject.Inject
+
+class GetBleNamesFlow @Inject constructor(
+ private val bleNameRepository: BleNameRepository
+) {
+
+ operator fun invoke(): Flow> {
+
+ return bleNameRepository.getNamesFlow()
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetFoundBle.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetFoundBle.kt
new file mode 100644
index 0000000..96ea9a2
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetFoundBle.kt
@@ -0,0 +1,17 @@
+package llc.arma.ble.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.model.BleInfo
+import llc.arma.ble.domain.repository.BleRepository
+import javax.inject.Inject
+
+class GetFoundBle @Inject constructor(
+ private val bleRepository: BleRepository
+) {
+
+ operator fun invoke(): List = bleRepository.getFoundBle()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetHostBleTableBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostBleTableBySerial.kt
new file mode 100644
index 0000000..e0fcedb
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostBleTableBySerial.kt
@@ -0,0 +1,27 @@
+package llc.arma.ble.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onEach
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.model.Rotation
+import llc.arma.ble.domain.repository.BleRepository
+import llc.arma.ble.domain.repository.RotationsRepository
+import java.time.LocalDateTime
+import java.util.Date
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class GetHostBleTableBySerial @Inject constructor(
+ private val bleRepository: BleRepository,
+) {
+
+ suspend operator fun invoke(serial: String): Result, BleException> {
+
+ return bleRepository.getHostBleTableBySerial(serial)
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt
new file mode 100644
index 0000000..109cf57
--- /dev/null
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetHostHistoryBySerial.kt
@@ -0,0 +1,21 @@
+package llc.arma.ble.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import llc.arma.ble.domain.Result
+import llc.arma.ble.domain.common.BleException
+import llc.arma.ble.domain.common.ProgressState
+import llc.arma.ble.domain.model.Ble
+import llc.arma.ble.domain.repository.BleRepository
+import javax.inject.Inject
+
+class GetHostHistoryBySerial @Inject constructor(
+ private val bleRepository: BleRepository,
+) {
+
+ suspend operator fun invoke(serial: String): Flow>, BleException>> {
+
+ return bleRepository.getHostHistoryBySerial(serial)
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
index 1732211..381797d 100644
--- a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
+++ b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt
@@ -1,6 +1,5 @@
package llc.arma.ble.domain.usecase
-import android.app.appsearch.SetSchemaRequest
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository
@@ -24,6 +23,13 @@ class WriteBle @Inject constructor(
return bleRepository.writeBle(serial, request)
}
+ suspend operator fun invoke(
+ serial: String,
+ request: Ble.Host.WriteRequest
+ ): llc.arma.ble.domain.Result{
+ return bleRepository.writeBle(serial, request)
+ }
+
suspend operator fun invoke(
serial: String,
request: Ble.Accelerometer.WriteRequest
diff --git a/build.gradle b/build.gradle
index dd43d6b..cff2060 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,11 +1,11 @@
buildscript {
ext {
compose_version = '1.3.3'
- kotlin_version = '1.8.10'
+ kotlin_version = '1.9.22'
}
dependencies {
- classpath('com.google.dagger:hilt-android-gradle-plugin:2.45')
+ classpath('com.google.dagger:hilt-android-gradle-plugin:2.46')
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10"
}
repositories {
@@ -16,5 +16,6 @@ buildscript {
plugins {
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
- id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
+ id("androidx.room") version "2.6.1" apply false
}
\ No newline at end of file