host table sync

This commit is contained in:
Vineyro 2024-08-01 16:47:51 +07:00
parent 80bf7197e7
commit 217281b579
48 changed files with 4319 additions and 97 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.10" />
<option name="version" value="1.9.22" />
</component>
</project>

View File

@ -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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,7 +122,9 @@ fun BleListScreen(
}
)
val filteredData = state.bleList.filter {
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) &&
@ -132,7 +134,12 @@ fun BleListScreen(
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.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 }
}
@ -145,6 +152,8 @@ fun BleListScreen(
}
}
if(filteredData.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
@ -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)
) {
}

View File

@ -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(

View File

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

View File

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

View File

@ -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<String>("serial")!!)
}
}
is HostContract.Effect.Navigation.NavigateToHostHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble
)
}
}
is HostContract.Effect.Navigation.NavigateToBleTable -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial
)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnBeaconNavigationEvent

View File

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

View File

@ -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<HostViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val bottomDialog = rememberBottomDialogState()
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is HostContract.Effect.Navigation -> onNavigationEvent(it)
HostContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
HostContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
HostContract.Effect.HidePowerPicker -> launch {
sheetPage = null
}
HostContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER_EDIT
}
HostContract.Effect.HideIntervalPicker -> launch {
sheetPage = null
}
HostContract.Effect.ShowIntervalPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL_EDIT
}
}
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(HostContract.Event.OnBleChanged(ble))
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.POWER_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display) {
PowerEdit(
state = currentState.host,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display) {
IntervalEdit(
state = currentState.host,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Column {
when(state){
is HostContract.State.Display -> DisplayState(
onEvent = {
viewModel.setEvent(it)
},
ble = state.host,
origin = state.origin
)
is HostContract.State.Loading -> LoadingState()
}
}
}
@Composable
private fun LoadingState(){
Box(modifier = Modifier.fillMaxSize()){
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}

View File

@ -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<HostContract.State, HostContract.Event, HostContract.Effect>() {
override fun setInitialState() = HostContract.State.Loading
override fun handleEvents(event: HostContract.Event) {
when(event){
is HostContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is HostContract.Event.OnTxChanged -> reduce(viewState.value, event)
is HostContract.Event.OnBleChanged -> reduce(viewState.value, event)
is HostContract.Event.OnChangePassword -> reduce(viewState.value, event)
is HostContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is HostContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is HostContract.Event.OnWriteBle -> reduce(viewState.value, event)
is HostContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is HostContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is HostContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
is HostContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
is HostContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is HostContract.Event.OnShowIntervalEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnSaveIntervalChanged
) {
if(state is HostContract.State.Display) {
state.host.hostState.historyInterval = event.interval
}
setEffect {
HostContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowIntervalEdit
) {
setEffect {
HostContract.Effect.ShowIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowHostBleTable
) {
if(state is HostContract.State.Display) {
setEffect {
HostContract.Effect.Navigation.NavigateToBleTable(state.host.info.serial)
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowHostHistory
) {
if(state is HostContract.State.Display) {
setEffect {
HostContract.Effect.Navigation.NavigateToHostHistory(state.host.info)
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnPowerChanged
) {
if(state is HostContract.State.Display) {
state.host.state.tx = event.tx
}
setEffect {
HostContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnPowerEdit
) {
setEffect { HostContract.Effect.ShowPowerPicker }
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnNavigateUpClicked
) {
setEffect { HostContract.Effect.Navigation.NavigateUp }
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnTxChanged
) {
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnBleChanged
) {
when(state){
is HostContract.State.Display -> {
setState {
state.copy(
origin = Ble.Host(
info = event.ble.info,
state = state.origin.state,
hostState = state.origin.hostState
)
)
}
}
is HostContract.State.Loading -> {
setState {
HostContract.State.Display(
origin = event.ble,
host = bleMapper.map(event.ble) as BleView.Host,
writeState = null
)
}
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnChangePassword
) {
setEffect {
HostContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnHideWriteBlePreview
) {
setEffect {
HostContract.Effect.HideWriteBlePreview
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowWriteBlePreview
) {
if(state is HostContract.State.Display){
val newBle = bleViewMapper.map(state.host) as Ble.Host
val writeRequest = Ble.Host.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
interval = if(newBle.hostState.historyInterval == state.origin.hostState.historyInterval) null else newBle.hostState.historyInterval
)
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
HostContract.Effect.ShowWriteBlePreview
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnWriteBle
) {
if(state is HostContract.State.Display){
state.writeState?.let { request ->
if(request is HostContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is HostContract.State.Display) {
val newBleObject = Ble.Host(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
hostState = currentState.origin.hostState.copy(
historyInterval = request.writeRequest.interval
?: currentState.origin.hostState.historyInterval
)
)
writeBle(state.host.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
host = bleMapper.map(newBleObject) as BleView.Host,
writeState = HostContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}
}

View File

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

View File

@ -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<HostHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(HostHistoryContract.Event.OnStart(ble.name, ble.serial))
}
/*DisposableEffect("ble") {
onDispose {
viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure)
}
}*/
Column() {
TopAppBar(
navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = null
)
}
}
},
title = {
val title = when(state){
is HostHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "Таблица (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "Таблица"
is ProgressState.Progress -> "Таблица"
}
}
HostHistoryContract.State.Exception -> "Таблица"
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(HostHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial))
},
enabled = when(state){
is HostHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
HostHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
Box(modifier = Modifier) {
when (state) {
is HostHistoryContract.State.Display -> Display(state = state)
is HostHistoryContract.State.Exception -> Exception()
}
}
}
}
val dayFormatter = SimpleDateFormat("dd", Locale.getDefault())
val dateFormatter = SimpleDateFormat("dd.MM", Locale.getDefault())
val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
val colorsStack = listOf(
-0x63d850, -0x98c549, -0xc0ae4b, -0xde690d,
-0xfc560c, -0xff432c, -0xff6978, -0xb350b0,
-0x743cb6, -0x3223c7, -0x14c5, -0x3ef9,
-0x6800, -0xa8de, -0x86aab8, -0x616162,
-0x9f8275, -0xcccccd, -0xbbcca
)
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
val first = (chartValues.chartEntryModel.entries.firstOrNull()?.firstOrNull() as? HostEntry)
val last = (chartValues.chartEntryModel.entries.firstOrNull()?.lastOrNull() as? HostEntry)
val previous = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt() - 1) as? HostEntry)
val current = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt()) as? HostEntry)
if(current != null) {
if (first == current || last == current) {
dateFormatter.format(Date(current.localDate))
} else {
if(previous != null && dayFormatter.format(previous.localDate) != dayFormatter.format(current.localDate)){
dateFormatter.format(Date(current.localDate))
}else{
timeFormatter.format(Date(current.localDate))
}
}.orEmpty()
} else {
" "
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Display(
state: HostHistoryContract.State.Display
) {
val configuration = LocalConfiguration.current
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val allSerials = remember(state) { state.loadingHistoryState.data.flatMap { it.value }.distinct() }
val colors = remember(allSerials) {
allSerials.mapIndexed { index, s ->
Pair(s, colorsStack[index])
}.toMap()
}
var selectedSerials by remember {
mutableStateOf(allSerials)
}
val serials = remember(selectedSerials) { allSerials.filter { selectedSerials.contains(it) } }
val entries = remember(serials, state) {
serials.map { serial ->
ChartEntryModelProducer(
state.loadingHistoryState.data.mapIndexed { index, historyPoint ->
if(historyPoint.value.contains(serial)) {
HostEntry(historyPoint.date, index.toFloat(), 1f)
} else {
HostEntry(historyPoint.date, index.toFloat(), 0f)
}
}
)
}
}
val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
val chart = columnChart(
innerSpacing = 2.dp,
columns = serials.map { LineComponent(color = colors[it]!!, thicknessDp = 7f, shape= pillShape) },
spacing = 8.dp,
)
@Composable
fun LegendItem(s: String) {
FilterChip(
selected = selectedSerials.contains(s),
onClick = {
selectedSerials = if(selectedSerials.contains(s)){
selectedSerials.toMutableList().apply {
remove(s)
}
}else{
selectedSerials.toMutableList().apply {
add(s)
}
}
},
leadingIcon = {
Surface(
shape = CircleShape,
color = Color(colors[s]!!),
modifier = Modifier.size(28.dp)
) {}
},
label = { Column {
Text(text = state.bleNames.firstOrNull { it.serial == s }?.name ?: s)
Text(
style = MaterialTheme.typography.bodySmall.copy(
color = LocalTextStyle.current.color.copy(
alpha = ContentAlpha.medium
)
),
text = s
)
}}
)
}
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
modifier = Modifier
.fillMaxHeight()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
VerticalDivider()
FlowRow(
maxItemsInEachRow = 1,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.verticalScroll(rememberScrollState())
) {
allSerials.mapIndexed { index, s ->
LegendItem(s = s)
}
}
}
}
else -> {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
FlowColumn(
maxItemsInEachColumn = 2,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
allSerials.mapIndexed { index, s ->
LegendItem(s = s)
}
}
HorizontalDivider()
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
}
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class HostHistoryContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val bleName: String,
val serial: String,
) : Event()
data class OnRefreshHistory(
val bleName: String,
val serial: String,
) : Event()
}
sealed class State : ViewState {
data class Display(
val bleName: String,
val bleNames: List<BleName>,
val loadingHistoryState : ProgressState<List<Ble.Host.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class HostHistoryViewModel @Inject constructor(
private val getHostHistoryBySerial: GetHostHistoryBySerial,
private val getBleBySerial: GetBleBySerial,
private val getBleNamesFlow: GetBleNamesFlow
) : BaseViewModel<HostHistoryContract.State, HostHistoryContract.Event, HostHistoryContract.Effect>() {
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = HostHistoryContract.State.Display(
"",
emptyList(),
ProgressState.Indeterminate
)
override fun handleEvents(event: HostHistoryContract.Event) {
when(event){
is HostHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is HostHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is HostHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
HostHistoryContract.State.Exception
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is HostHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
HostHistoryContract.State.Display(event.bleName, emptyList(), ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
val names = getBleNamesFlow().first()
measureJob = getHostHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
HostHistoryContract.State.Display(event.bleName, names, it)
}
},
onFailure = {
setState {
HostHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
HostHistoryContract.State.Display("", emptyList(), ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
val names = getBleNamesFlow().first()
measureJob = getHostHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
HostHistoryContract.State.Display(event.bleName, names, it)
}
},
onFailure = {
setState {
HostHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

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

View File

@ -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 = "Применить"
)
}
}
}
}

View File

@ -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 = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -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<BleInfo>,
val newBle: List<BleInfo>,
val bleTable: List<String>,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: List<BleInfo>
) : WriteState()
data class Writing(
val writeRequest: List<BleInfo>
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object NavigateUp : Navigation()
}
}
}

View File

@ -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<BleTableEditViewModel>()
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<BleInfo?>(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<String>,
selected: List<BleInfo>,
bleList: List<BleInfo>,
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)
)
}
}

View File

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

View File

@ -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 = "Ок"
)
}
}
}
}
}
}
}
}

View File

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

View File

@ -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<BleNameEntity>)
@Query("select * from ble_name")
fun getAllFlow(): Flow<List<BleNameEntity>>
}

View File

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

View File

@ -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<BleName>) {
bleNameDao.save(
names.map {
BleNameEntity(
serial = it.serial,
name = it.name
)
}
)
}
override fun getNamesFlow(): Flow<List<BleName>> {
return bleNameDao.getAllFlow().map { list ->
list.map {
BleName(
serial = it.serial,
name = it.name
)
}
}
}
}

View File

@ -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<String, BleInfo> = Collections.synchronizedMap(mutableMapOf<String, BleInfo>())
override fun getFoundBle(): List<BleInfo> {
return resultList.values.toList()
}
override fun getBleAroundFlow(): Result<Flow<List<BleInfo>>, 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<Ble.Host.HostState, BleException> {
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<List<String>, BleException> {
return readHostBleTable(serial, app)
}
override suspend fun addBleToHostTableBySerial(serial: String, ble: List<String>): Result<Int, BleException> {
return addBleToHostTable(serial, ble, app)
}
override suspend fun getHostHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Host.HistoryPoint>>, BleException>> {
return readHostHistory(serial, app)
}
override suspend fun getAccelerometerHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
@ -577,13 +700,75 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun writeBle(
serial: String,
request: Ble.Accelerometer.WriteRequest
request: Ble.Host.WriteRequest
): Result<Unit, BleException> {
fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
(3 downTo 0).map {
(this shr (it * Byte.SIZE_BITS)).toByte()
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<Byte>(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<Unit, BleException> {
rotationsDao.deleteBySerial(serial)

View File

@ -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,

View File

@ -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<Byte> {
characteristic.write(DataByteArray(startRequest))
var value = characteristic.read().value
var nextPackageDataCount = value.get2byteUIntAt(2)
val tableResult = mutableListOf<Byte>()
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<Result<ProgressState<List<Ble.Host.HistoryPoint>>, 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<Byte> = mutableListOf()
val secondTablePackage: MutableList<Byte> = 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<List<String>>()
var periodBle = mutableListOf<String>()
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<List<String>, 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<String>,
app: Application,
): Result<Int, BleException> {
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)
}
}

View File

@ -36,11 +36,11 @@ fun readThermometerHistory(
if (app.checkPermission()) {
try {
val connection =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
?.findCharacteristic(temperatureHistoryReadUUID)
@ -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 {

View File

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

View File

@ -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 {

View File

@ -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<String>
)
data class HostState(
val historyInterval: Long
)
data class WriteRequest(
val tx: BleState.TX?,
val interval: Long?
)
}
class Thermometer(
info: BleInfo,
val state: BleState,

View File

@ -16,7 +16,7 @@ data class BleInfo(
) : Parcelable {
enum class Type {
BEACON, THERMOMETER, ACCELEROMETER
HOST, BEACON, THERMOMETER, ACCELEROMETER
}
}

View File

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

View File

@ -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<BleName>)
fun getNamesFlow(): Flow<List<BleName>>
}

View File

@ -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<BleInfo>
fun getBleAroundFlow(): Result<Flow<List<BleInfo>>, BleException>
suspend fun getBleBySerial(serial: String) : Result<Flow<Ble>, BleException>
suspend fun getTemperatureHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>>
suspend fun getTemperatureHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>>
suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(
serial: String,
request: Ble.Thermometer.WriteRequest
): Result<Unit, BleException>
suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(
serial: String,
request: Ble.Beacon.WriteRequest
): Result<Unit, BleException>
suspend fun writeBle(serial: String, request: Ble.Accelerometer.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(
serial: String,
request: Ble.Host.WriteRequest
): Result<Unit, BleException>
suspend fun changeBlePassword(password: String, serial: String): Result<Unit, BleException>
suspend fun writeBle(
serial: String,
request: Ble.Accelerometer.WriteRequest
): Result<Unit, BleException>
suspend fun changeBlePassword(
password: String,
serial: String
): Result<Unit, BleException>
fun getAccelerometerMeasureBySerialFlow(
serial: String,
@ -47,6 +67,21 @@ interface BleRepository {
frequency: FftFrequency
): Flow<Result<ProgressState<List<Ble.Accelerometer.SpectrePoint>>, BleException>>
suspend fun getAccelerometerHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>>
suspend fun getAccelerometerHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>>
suspend fun getHostHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Host.HistoryPoint>>, BleException>>
suspend fun getHostBleTableBySerial(
serial: String
): Result<List<String>, BleException>
suspend fun addBleToHostTableBySerial(
serial: String,
ble: List<String>
): Result<Int, BleException>
}

View File

@ -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<BleInfo>): Result<Int, BleException> {
bleNameRepository.save(
ble.map {
BleName(
serial = it.serial,
name = it.name
)
}
)
return bleRepository.addBleToHostTableBySerial(serial, ble.map { it.serial })
}
}

View File

@ -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<List<BleName>> {
return bleNameRepository.getNamesFlow()
}
}

View File

@ -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<BleInfo> = bleRepository.getFoundBle()
}

View File

@ -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<List<String>, BleException> {
return bleRepository.getHostBleTableBySerial(serial)
}
}

View File

@ -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<Result<ProgressState<List<Ble.Host.HistoryPoint>>, BleException>> {
return bleRepository.getHostHistoryBySerial(serial)
}
}

View File

@ -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<Unit, BleException>{
return bleRepository.writeBle(serial, request)
}
suspend operator fun invoke(
serial: String,
request: Ble.Accelerometer.WriteRequest

View File

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