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