total refactor

This commit is contained in:
Vineyro 2025-06-16 12:24:55 +07:00
parent 435a4db2fb
commit 39297abc6c
136 changed files with 5954 additions and 6910 deletions

View File

@ -4,7 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-02-12T02:51:30.451430800Z"> <DropdownSelection timestamp="2025-06-11T08:44:02.119013Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=BV5900DNS00004122" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=BV5900DNS00004122" />

View File

@ -106,11 +106,12 @@ dependencies {
ksp(libs.hilt.android.compiler) ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)
implementation(libs.scanner) //implementation(libs.scanner)
implementation(libs.client) //implementation(libs.client)
//implementation("no.nordicsemi.kotlin.ble:core:2.0.0-alpha02") //implementation("no.nordicsemi.kotlin.ble:core:2.0.0-alpha02")
implementation("no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha02") implementation("no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha02")
implementation("org.slf4j:slf4j-simple:2.1.0-alpha1")
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)

View File

@ -7,70 +7,45 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.SurfaceView
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BottomState
import llc.arma.ble.app.ui.common.LocalBottomDialogState
import llc.arma.ble.app.ui.screen.main.MainScreen import llc.arma.ble.app.ui.screen.main.MainScreen
import llc.arma.ble.app.ui.theme.BleTheme import llc.arma.ble.app.ui.theme.BleTheme
import org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
init {
System.setProperty(DEFAULT_LOG_LEVEL_KEY, "Debug")
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -78,102 +53,12 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
//installSplashScreen() installSplashScreen()
setContent { setContent {
BleTheme { BleTheme {
val modalState =
rememberModalBottomSheetState(
skipHalfExpanded = true,
initialValue = ModalBottomSheetValue.Hidden
)
var sheetContent by remember() {
mutableStateOf<@Composable () -> Unit>({})
}
if(modalState.currentValue == ModalBottomSheetValue.Hidden){
sheetContent = {}
}
CompositionLocalProvider(
LocalBottomDialogState provides BottomState(
sheetState = modalState,
setContent = {
sheetContent = it
}
)
) {
BoxWithConstraints(
modifier = Modifier.navigationBarsPadding()
) {
val maxHeight = with(LocalDensity.current) {
this@BoxWithConstraints.constraints.maxHeight.toDp()
}
ModalBottomSheetLayout(
modifier = Modifier,
sheetShape = RoundedCornerShape(
topStart = 25.dp,
topEnd = 25.dp
),
sheetElevation = 0.dp,
sheetState = modalState,
sheetContent = {
val statusBarHeight = with(LocalDensity.current) {
WindowInsets.statusBars.getTop(this).toDp()
}
val scope = rememberCoroutineScope()
Column(
modifier = Modifier.heightIn(max = maxHeight - statusBarHeight),
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.navigationBarsPadding()
) {
Spacer(modifier = Modifier.height(14.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f),
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(
width = 54.dp,
height = 5.dp
)
) {}
Spacer(modifier = Modifier.height(12.dp))
sheetContent()
}
}
}
BackHandler(modalState.isVisible) {
scope.launch { modalState.hide() }
}
},
content = {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Surface( Surface(
modifier = Modifier modifier = Modifier
@ -220,7 +105,7 @@ class MainActivity : ComponentActivity() {
mutableStateOf(mBluetoothAdapter.isEnabled) mutableStateOf(mBluetoothAdapter.isEnabled)
} }
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState() val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LaunchedEffect(lifecycleState) { LaunchedEffect(lifecycleState) {
@ -260,14 +145,6 @@ class MainActivity : ComponentActivity() {
} }
)
}
}
}
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,64 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RetryingLoadingTemplate(
attempt: Int?,
onCancel: () -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.widthIn(max = 230.dp)
) {
ContainedLoadingIndicator()
attempt?.let {
Spacer(Modifier.height(16.dp))
Text(
text = "Повторная попытка ${it}"
)
Text(
text = "Во время загрузки произошла ошибка",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
TextButton(
onClick = onCancel
) {
Text(
text = "Отмена"
)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp

View File

@ -3,7 +3,6 @@ package llc.arma.ble.app.ui.screen.ble
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
class BleListContract { class BleListContract {
@ -22,8 +21,18 @@ class BleListContract {
data class State( data class State(
val bleList: List<BleInfo>, val bleList: List<BleInfo>,
val bleFilter: BleFilter val filterIsEmpty: Boolean,
) : ViewState val summary: BleSummary
) : ViewState {
data class BleSummary(
val all: Int,
val lowBattery: Int,
val lost: Int,
val active: Int
)
}
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {

View File

@ -1,7 +1,6 @@
package llc.arma.ble.app.ui.screen.ble package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock import android.os.SystemClock
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -20,17 +19,21 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.Battery1Bar
import androidx.compose.material.icons.rounded.BatteryFull import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.FilterAlt import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Link import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Summarize
import androidx.compose.material.icons.rounded.TimerOff
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -38,21 +41,22 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -63,25 +67,20 @@ import com.ramcosta.composedestinations.generated.destinations.AccelerometerScre
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BleFilterScreenDestination import com.ramcosta.composedestinations.generated.destinations.BleFilterScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.icon import llc.arma.ble.app.ui.screen.locale.icon
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow import kotlin.math.pow
@Destination<RootGraph>(start = true) @Destination<RootGraph>(start = true)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun BleListScreen( fun BleListScreen(
//onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit //onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
@ -131,47 +130,16 @@ fun BleListScreen(
}, },
actions = { actions = {
Row( FilledIconToggleButton(
modifier = Modifier checked = state.filterIsEmpty.not(),
.padding(horizontal = 8.dp) onCheckedChange = {
.align(Alignment.CenterVertically)
) {
Text(text = "${state.bleList.size}")
Spacer(modifier = Modifier.width(12.dp))
Text(text = "${state.bleList.filter {
it.batteryLevel == 100
}.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
Text(text = " | ")
Text(
text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
Text(text = " | ")
Text(
text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
color = MaterialTheme.colorScheme.error
)
}
IconButton(
onClick = {
viewModel.setEvent(BleListContract.Event.OnShowFilter) viewModel.setEvent(BleListContract.Event.OnShowFilter)
} }
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.FilterAlt, imageVector = Icons.Rounded.FilterAlt,
contentDescription = null contentDescription = null
) )
} }
} }
@ -183,39 +151,9 @@ fun BleListScreen(
modifier = Modifier.padding(it) modifier = Modifier.padding(it)
) { ) {
val filteredData = remember(state.bleList, state.bleFilter) { var showSummary by remember { mutableStateOf(false) }
state.bleList.filter { if(state.bleList.isEmpty()){
(it.type == state.bleFilter.bleType || state.bleFilter.bleType == null) &&
it.name.contains(state.bleFilter.name) &&
it.serial.contains(state.bleFilter.mac) &&
state.bleFilter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
state.bleFilter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (state.bleFilter.sortField) {
BleFilter.Field.Name -> it.sortedBy { it.name }
BleFilter.Field.Mac -> it.sortedBy { it.serial }
BleFilter.Field.Distance -> it.sortedBy {
10.0.pow(
(it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
).toFloat()
}
BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
}
}.let {
when (state.bleFilter.sortOrder) {
BleFilter.Order.Asc -> it
BleFilter.Order.Desc -> it.reversed()
}
}
}
if(filteredData.isEmpty()){
LinearProgressIndicator( LinearProgressIndicator(
strokeCap = StrokeCap.Round, strokeCap = StrokeCap.Round,
modifier = Modifier modifier = Modifier
@ -224,7 +162,7 @@ fun BleListScreen(
) )
} }
if(filteredData.isEmpty()){ if(state.bleList.isEmpty()){
Box(modifier = Modifier.fillMaxSize()){ Box(modifier = Modifier.fillMaxSize()){
Text( Text(
@ -242,14 +180,130 @@ fun BleListScreen(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
item {
SummaryItem(
shape = if(showSummary) ShapeType.Start else ShapeType.Singleton,
onClick = { showSummary = showSummary.not() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
if(showSummary) {
Text("Итоги")
} else {
Text("${state.summary.all} - ${state.summary.active} | ")
Text(
text = "${state.summary.lost}",
color = LocalContentColor.current.copy(alpha = 0.5f)
)
Text(" | ")
Text(
text = "${state.summary.lowBattery}",
color = MaterialTheme.colorScheme.error
)
}
Spacer(
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.rotate(if(showSummary) 180f else 0f)
)
}
}
}
if(showSummary) {
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.Summarize,
onClick = { showSummary = true }
) {
Text("Всего: ${state.summary.all}")
}
}
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.Bluetooth
) {
Text("Активные: ${state.summary.active}")
}
}
item {
SummaryItem(
shape = ShapeType.Middle,
icon = Icons.Rounded.TimerOff,
contentColor = LocalContentColor.current.copy(alpha = 0.7f)
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "Потерянные: ${state.summary.lost}"
)
}
}
item {
SummaryItem(
contentColor = MaterialTheme.colorScheme.error,
shape = ShapeType.End,
icon = Icons.Rounded.Battery1Bar
) {
Text("Разряженные: ${state.summary.lowBattery}")
}
}
}
item {
Spacer(
modifier = Modifier.height(12.dp)
)
}
items( items(
items = filteredData, items = state.bleList,
key = { it.serial } key = { it.serial }
) { ) {
BleItem( BleItem(
ble = it, ble = it,
shapeType = filteredData.takeShapeType(it), shapeType = state.bleList.takeShapeType(it),
onClick = { onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial)) viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
} }
@ -264,7 +318,48 @@ fun BleListScreen(
} }
}
@Composable
fun SummaryItem(
shape: ShapeType,
icon: ImageVector? = null,
contentColor: Color = Color.Unspecified,
onClick: (() -> Unit)? = null,
label: @Composable () -> Unit
){
Surface(
shape = shape.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.fillMaxWidth()
) {
CompositionLocalProvider(LocalContentColor provides contentColor) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.clickable { onClick?.invoke() }.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if(icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
}
label()
}
}
}
} }
@ -301,6 +396,7 @@ private fun Int.toSignalLevel(): Int {
fun BleItem( fun BleItem(
shapeType: ShapeType, shapeType: ShapeType,
ble: BleInfo, ble: BleInfo,
checked: Boolean,
onClick: () -> Unit onClick: () -> Unit
){ ){
@ -313,7 +409,6 @@ fun BleItem(
val highAlpha = ContentAlpha.high val highAlpha = ContentAlpha.high
val disabledAlpha = ContentAlpha.disabled val disabledAlpha = ContentAlpha.disabled
var time by remember { var time by remember {
mutableLongStateOf( mutableLongStateOf(
SystemClock.elapsedRealtime() SystemClock.elapsedRealtime()
@ -333,27 +428,118 @@ fun BleItem(
highAlpha highAlpha
} }
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 12.dp
)
) {
BleItemContent(
ble = ble,
color = color,
time = time,
)
Checkbox(
checked = checked,
onCheckedChange = null
)
}
}
}
@Composable
fun BleItem(
shapeType: ShapeType,
ble: BleInfo,
onClick: () -> Unit
){
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.surfaceContainer
}
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
)
}
LaunchedEffect(ble.scanTime) {
while(true) {
time = SystemClock.elapsedRealtime()
delay(1000)
}
}
val alpha = if(time - ble.scanTime > 10_000){
ContentAlpha.disabled
} else {
ContentAlpha.high
}
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
BleItemContent(
ble = ble,
color = color,
time = time,
modifier = Modifier.padding(
horizontal = 16.dp, vertical = 12.dp
)
)
}
}
@Composable
private fun BleItemContent(
ble: BleInfo,
color: Color,
time: Long,
modifier: Modifier = Modifier
){
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier modifier = modifier.fillMaxWidth()
.fillMaxWidth()
.clip(shapeType.shape)
.background(color)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp)
.alpha(alpha)
) { ) {
Box { Box {
ItemIcon { ItemIcon {
Icon( Icon(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
imageVector = ble.type.icon, imageVector = ble.type.icon,
contentDescription = null contentDescription = null
) )
} }
if(ble.tableStatus !== BleInfo.HistoryTableStatus.DISABLED){ if(ble.tableStatus !== BleInfo.HistoryTableStatus.DISABLED){
@ -403,7 +589,7 @@ fun BleItem(
modifier = Modifier.alpha(0.7f) modifier = Modifier.alpha(0.7f)
) { ) {
val color = if(ble.batteryLevel < 100){ val contentColor = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
} else { } else {
LocalContentColor.current LocalContentColor.current
@ -413,7 +599,7 @@ fun BleItem(
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.BatteryFull, imageVector = Icons.Rounded.BatteryFull,
contentDescription = null, contentDescription = null,
tint = color tint = contentColor
) )
Box { Box {
@ -427,7 +613,7 @@ fun BleItem(
Text( Text(
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
text = ble.batteryLevel.toString() + " %", text = ble.batteryLevel.toString() + " %",
color = color color = contentColor
) )
} }
@ -447,16 +633,8 @@ fun BleItem(
modifier = Modifier.alpha(0.7f) modifier = Modifier.alpha(0.7f)
) { ) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
val distance = remember(ble.rssi, ble.tx) { val distance = remember(ble.rssi, ble.tx) {
String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м." String.format("%.2f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
} }
Text( Text(

View File

@ -1,17 +1,18 @@
package llc.arma.ble.app.ui.screen.ble package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.GetBleAroundFlow import llc.arma.ble.domain.usecase.GetBleAroundFlow
import llc.arma.ble.domain.usecase.filter.GetFilterFlow import llc.arma.ble.domain.usecase.filter.GetFilterFlow
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.pow
@HiltViewModel @HiltViewModel
class BleListViewModel @Inject constructor( class BleListViewModel @Inject constructor(
@ -19,25 +20,8 @@ class BleListViewModel @Inject constructor(
private val getBleAroundFlow: GetBleAroundFlow private val getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() { ) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() {
private var scannerJob: Job? = null
init {
viewModelScope.launch {
getFilterFlow.invoke().onEach {
setState {
copy(
bleFilter = it
)
}
}.launchIn(this)
}
}
override fun setInitialState(): BleListContract.State = override fun setInitialState(): BleListContract.State =
BleListContract.State(emptyList(), BleFilter()) BleListContract.State(emptyList(), false, BleListContract.State.BleSummary(0,0,0,0))
override fun handleEvents(event: BleListContract.Event) { override fun handleEvents(event: BleListContract.Event) {
when(event){ when(event){
@ -86,6 +70,8 @@ class BleListViewModel @Inject constructor(
} }
} }
private var scannerJob: Job? = null
private fun reduce( private fun reduce(
state: BleListContract.State, state: BleListContract.State,
event: BleListContract.Event.OnResetScanner event: BleListContract.Event.OnResetScanner
@ -93,12 +79,50 @@ class BleListViewModel @Inject constructor(
scannerJob?.cancel() scannerJob?.cancel()
scannerJob = getBleAroundFlow().onEach { scannerJob = getFilterFlow().combine(getBleAroundFlow()){ filter, ble ->
val bleList = ble.filter {
(it.type == filter.bleType || filter.bleType == null) &&
it.name.contains(filter.name) &&
it.serial.contains(filter.mac) &&
filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
filter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (filter.sortField) {
BleFilter.Field.Name -> it.sortedBy { it.name }
BleFilter.Field.Mac -> it.sortedBy { it.serial }
BleFilter.Field.Distance -> it.sortedBy {
10.0.pow(
(it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
).toFloat()
}
BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
}
}.let {
when (filter.sortOrder) {
BleFilter.Order.Asc -> it
BleFilter.Order.Desc -> it.reversed()
}
}
setState { setState {
copy( BleListContract.State(
bleList = it bleList,
filter == BleFilter(),
BleListContract.State.BleSummary(
all = ble.size,
active = ble.filter { it.batteryLevel == 100 }
.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size,
lost = ble.count { SystemClock.elapsedRealtime() - it.scanTime > 10_000 },
lowBattery = ble.count { it.batteryLevel < 100 }
)
) )
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }

View File

@ -1,129 +0,0 @@
package llc.arma.ble.app.ui.screen.connection
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class ConnectionContract {
sealed class Event : ViewEvent {
data object RefreshBle : Event()
data object OnNavigateUp : Event()
data class OnBeaconNavigationEvent(
val event: BeaconContract.Effect.Navigation
) : Event()
data class OnHostNavigationEvent(
val event: GateContract.Effect.Navigation
) : Event()
data class OnThermometerNavigationEvent(
val event: ThermometerContract.Effect.Navigation
) : Event()
data class OnAccelNavigationEvent(
val event: AccelerometerContract.Effect.Navigation
) : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class DisplayException(
val tries: Long,
val exception: BleException
) : State()
data class Display(
val ble: Ble
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object NavigateUp : Navigation()
data class NavigateToChangePassword(
val serial: String
) : Navigation()
data class NavigateToRotationsStatistic(
val serial: String
) : Navigation()
data class NavigateToThermometerHistory(
val bleSerial: String
) : Navigation()
}
sealed class InnerNavigation : Effect() {
@Parcelize
data class NavigateToAccelHistory(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToAccelRealtime(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToAccelSpectre(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateToHostHistory(
val ble: BleInfo
) : InnerNavigation(), Parcelable
@Parcelize
data class NavigateHostToBleTable(
val serial: String
) : InnerNavigation(), Parcelable
}
}
}

View File

@ -1,326 +0,0 @@
package llc.arma.ble.app.ui.screen.connection
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.SmallPrimaryButton
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConnectionScreen(
onNavigationEvent: (ConnectionContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<ConnectionViewModel>()
val state = viewModel.viewState.value
var innerScreen by rememberSaveable {
mutableStateOf<ConnectionContract.Effect.InnerNavigation?>(null)
}
BackHandler(innerScreen != null) {
innerScreen = null
}
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is ConnectionContract.Effect.Navigation -> onNavigationEvent(it)
is ConnectionContract.Effect.InnerNavigation -> {
innerScreen = it
}
}
}.launchIn(this)
}
Box {
Column {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
if(innerScreen != null) {
innerScreen = null
} else {
viewModel.setEvent(ConnectionContract.Event.OnNavigateUp)
}
},
content = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
)
},
title = {
AnimatedContent(
targetState = when (state) {
is ConnectionContract.State.Display -> state.ble.info.name
is ConnectionContract.State.DisplayException -> "Исключение"
is ConnectionContract.State.Loading -> "Соединение.."
},
transitionSpec = {
((slideInVertically { height -> height } + fadeIn()).togetherWith(
slideOutVertically { height -> -height } + fadeOut())).using(
SizeTransform(clip = false)
)
}
) { targetText ->
Text(
text = targetText
)
}
}
)
/*when (state) {
is ConnectionContract.State.DisplayException -> DisplayException(
viewState = state,
onEvent = viewModel::setEvent
)
is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> {
when (state.ble) {
is Ble.Beacon -> BeaconScreen(
null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnBeaconNavigationEvent(
it
)
)
}
)
is Ble.Thermometer -> {
ThermometerScreen(
txSelectResult = null,
//ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnThermometerNavigationEvent(
it
)
)
}
)
}
is Ble.Accelerometer -> {
/*AccelerometerScreen {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
}*/
}
is Ble.Gate -> {
GateScreen(
null,
null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnHostNavigationEvent(it)
)
}
)
}
}
}
}*/
}
/*
innerScreen?.let {
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
when(it){
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory -> {
AccelerometerHistory(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
){
onNavigationEvent(ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic(it.ble.serial))
}
}
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime -> {
AccelerometerRealtime(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
)
}
is ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre -> {
AccelerometerSpectre(
ble = it.ble,
accelMode = it.accelMode,
fftAxis = it.fftAxis,
fftMode = it.fftMode,
frequency = it.frequency,
accelScale = it.accelScale,
onDismiss = {
innerScreen = null
}
)
}
is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> {
/*GateHistoryScreen(
ble = it.ble,
onDismiss = {
innerScreen = null
}
)*/
}
is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> {
GateBleTableScreen {
when(it){
GateBleTableContract.Effect.Navigation.Up -> {
innerScreen = null
}
}
}
}
}
}
}
*/
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
ContainedLoadingIndicator()
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun DisplayException(
viewState: ConnectionContract.State.DisplayException,
onEvent: (ConnectionContract.Event) -> Unit
){
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center).widthIn(max = 270.dp)
) {
ContainedLoadingIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
text = "Повторная попытка ${viewState.tries}"
)
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
text = "Неудалось соединится с устройством"
)
Spacer(modifier = Modifier.height(18.dp))
SmallPrimaryButton(
label = "Отмена"
) {
onEvent(ConnectionContract.Event.OnNavigateUp)
}
}
}
}

View File

@ -1,240 +0,0 @@
package llc.arma.ble.app.ui.screen.connection
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.usecase.GetBleBySerial
import javax.inject.Inject
@HiltViewModel
class ConnectionViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
) : BaseViewModel<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
init {
refreshBle()
}
override fun setInitialState() = ConnectionContract.State.Loading
override fun handleEvents(event: ConnectionContract.Event) {
when(event){
is ConnectionContract.Event.OnBeaconNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.OnHostNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.RefreshBle -> reduce(viewState.value, event)
is ConnectionContract.Event.OnAccelNavigationEvent -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnHostNavigationEvent
) {
/*when(event.event){
GateContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
is GateContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is GateContract.Effect.Navigation.GateHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble
)
}
}
is GateContract.Effect.Navigation.BleTable -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial
)
}
}
is GateContract.Effect.Navigation.TxSelector -> TODO()
is GateContract.Effect.Navigation.ReadIntervalSelector -> TODO()
}*/
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnBeaconNavigationEvent
) {
when(event.event){
BeaconContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
is BeaconContract.Effect.Navigation.PasswordForm -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is BeaconContract.Effect.Navigation.TxSelector -> TODO()
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnThermometerNavigationEvent
) {
/*(event.event){
ThermometerContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
ThermometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is ThermometerContract.Effect.Navigation.ThermometerHistory -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToThermometerHistory(event.event.bleSerial)
}
}
is ThermometerContract.Effect.Navigation.TxSelector -> TODO()
}*/
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
/*when(event.event){
is AccelerometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is AccelerometerContract.Effect.Navigation.AccelHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
is AccelerometerContract.Effect.Navigation.AccelRealtime -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
is AccelerometerContract.Effect.Navigation.AccelSpectre -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre(
event.event.ble,
event.event.accelScale,
event.event.accelMode,
event.event.fftAxis,
event.event.fftMode,
event.event.frequency
)
}
}
is AccelerometerContract.Effect.Navigation.TxPowerSelector -> TODO()
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector -> TODO()
is AccelerometerContract.Effect.Navigation.SaveIntervalSelector -> TODO()
}
*/
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnNavigateUp
) {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.RefreshBle
) {
refreshBle()
}
private fun refreshBle(){
/*val serial = savedStateHandle.get<String>("serial")
if(serial != null){
viewModelScope.launch {
setState {
ConnectionContract.State.Loading
}
var tries = 0L
while (true) {
getBleBySerial(serial, this).fold(
onSuccess = {
it.onEach {
setState {
ConnectionContract.State.Display(
ble = it
)
}
}.launchIn(viewModelScope)
return@launch
},
onFailure = {
setState {
tries += 1
ConnectionContract.State.DisplayException(tries, it)
}
}
)
}
}
} else {
throw IllegalArgumentException("serial arg must not be null")
}*/
}
}

View File

@ -26,6 +26,7 @@ class BleFilterContract {
data object Loading : State() data object Loading : State()
data class Display( data class Display(
val origin: BleFilter,
val filter: BleFilter val filter: BleFilter
) : State() ) : State()

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen.filter package llc.arma.ble.app.ui.screen.filter
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -152,11 +153,15 @@ private fun DisplayState(
state: BleFilterContract.State.Display state: BleFilterContract.State.Display
){ ){
Column {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.padding(16.dp) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
) { ) {
Surface( Surface(
@ -175,10 +180,12 @@ private fun DisplayState(
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState( val sortTextFileState = remember(state.filter.sortField) {
TextFieldState(
state.filter.sortField.localized, state.filter.sortField.localized,
TextRange(state.filter.sortField.localized.length) TextRange(state.filter.sortField.localized.length)
) )
}
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
modifier = Modifier modifier = Modifier
@ -220,11 +227,13 @@ private fun DisplayState(
BleFilter.Field.entries.forEach { selectionOption -> BleFilter.Field.entries.forEach { selectionOption ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy( state.filter.copy(
sortField = selectionOption sortField = selectionOption
) )
)) )
)
expanded = false expanded = false
}, },
text = { text = {
@ -243,10 +252,13 @@ private fun DisplayState(
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val sortTextFieldState = TextFieldState(
val sortTextFieldState = remember(state.filter.sortOrder) {
TextFieldState(
state.filter.sortOrder.localized, state.filter.sortOrder.localized,
TextRange(state.filter.sortOrder.localized.length) TextRange(state.filter.sortOrder.localized.length)
) )
}
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
modifier = Modifier modifier = Modifier
@ -286,9 +298,11 @@ private fun DisplayState(
BleFilter.Order.entries.forEach { selectionOption -> BleFilter.Order.entries.forEach { selectionOption ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(sortOrder = selectionOption) state.filter.copy(sortOrder = selectionOption)
)) )
)
expanded = false expanded = false
}, },
text = { text = {
@ -307,10 +321,13 @@ private fun DisplayState(
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
val typeTextFiledState = TextFieldState(
val typeTextFiledState = remember(state.filter.bleType) {
TextFieldState(
state.filter.bleType.localized, state.filter.bleType.localized,
TextRange(state.filter.bleType.localized.length) TextRange(state.filter.bleType.localized.length)
) )
}
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
modifier = Modifier modifier = Modifier
@ -353,9 +370,11 @@ private fun DisplayState(
}.forEach { selectionOption -> }.forEach { selectionOption ->
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(bleType = selectionOption) state.filter.copy(bleType = selectionOption)
)) )
)
expanded = false expanded = false
}, },
text = { text = {
@ -373,9 +392,11 @@ private fun DisplayState(
value = state.filter.name, value = state.filter.name,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(name = it) state.filter.copy(name = it)
)) )
)
}, },
label = { Text(text = "Имя") }, label = { Text(text = "Имя") },
leadingIcon = { leadingIcon = {
@ -390,9 +411,11 @@ private fun DisplayState(
IconButton( IconButton(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(name = "") state.filter.copy(name = "")
)) )
)
} }
) { ) {
Icon( Icon(
@ -414,9 +437,11 @@ private fun DisplayState(
value = state.filter.mac, value = state.filter.mac,
singleLine = true, singleLine = true,
onValueChange = { onValueChange = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(mac = it) state.filter.copy(mac = it)
)) )
)
}, },
label = { Text(text = "Mac") }, label = { Text(text = "Mac") },
leadingIcon = { leadingIcon = {
@ -431,9 +456,11 @@ private fun DisplayState(
IconButton( IconButton(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(mac = "") state.filter.copy(mac = "")
)) )
)
} }
) { ) {
Icon( Icon(
@ -476,12 +503,19 @@ private fun DisplayState(
Column { Column {
var sliderState by remember(state.filter.rssi) {
mutableStateOf(state.filter.rssi)
}
RangeSlider( RangeSlider(
value = state.filter.rssi, value = sliderState,
onValueChange = { onValueChange = {
sliderState = it
},
onValueChangeFinished = {
viewModel.setEvent( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged( BleFilterContract.Event.OnFilterChanged(
state.filter.copy(rssi = it) state.filter.copy(rssi = sliderState)
) )
) )
}, },
@ -498,7 +532,10 @@ private fun DisplayState(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text(text = state.filter.rssi.endInclusive.toInt().toString() + " dBm") Text(
text = state.filter.rssi.endInclusive.toInt()
.toString() + " dBm"
)
} }
@ -522,12 +559,19 @@ private fun DisplayState(
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
var sliderState by remember(state.filter.battery) {
mutableStateOf(state.filter.battery)
}
RangeSlider( RangeSlider(
value = state.filter.battery, value = sliderState,
onValueChange = { onValueChange = {
sliderState = it
},
onValueChangeFinished = {
viewModel.setEvent( viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged( BleFilterContract.Event.OnFilterChanged(
state.filter.copy(battery = it) state.filter.copy(battery = sliderState)
) )
) )
}, },
@ -544,7 +588,10 @@ private fun DisplayState(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text(text = state.filter.battery.endInclusive.toInt().toString() + " %") Text(
text = state.filter.battery.endInclusive.toInt()
.toString() + " %"
)
} }
@ -556,13 +603,22 @@ private fun DisplayState(
} }
Spacer(modifier = Modifier.height(8.dp)) }
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.filter != state.origin) {
Button( Button(
onClick = { onClick = {
viewModel.setEvent(BleFilterContract.Event.OnSave) viewModel.setEvent(BleFilterContract.Event.OnSave)
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) { ) {
Text( Text(
text = "Применить" text = "Применить"
@ -572,3 +628,7 @@ private fun DisplayState(
} }
} }
}
}

View File

@ -22,7 +22,7 @@ class BleFilterViewModel @Inject constructor(
val filter = getFilterFlow.invoke().firstOrNull() ?: BleFilter() val filter = getFilterFlow.invoke().firstOrNull() ?: BleFilter()
setState { BleFilterContract.State.Display(filter) } setState { BleFilterContract.State.Display(filter, filter) }
} }
@ -74,7 +74,15 @@ class BleFilterViewModel @Inject constructor(
state: BleFilterContract.State, state: BleFilterContract.State,
event: BleFilterContract.Event.OnFilterChanged, event: BleFilterContract.Event.OnFilterChanged,
) { ) {
setState { BleFilterContract.State.Display(event.filter) } if(state is BleFilterContract.State.Display) {
setState {
state.copy(
filter = event.filter
)
}
}
} }
private fun reduce( private fun reduce(

View File

@ -23,7 +23,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
@ -335,8 +338,6 @@ private fun DisplayState(
labelRotationDegrees = -90f, labelRotationDegrees = -90f,
) )
val scrollState = rememberChartScrollState()
when(lastMeasure){ when(lastMeasure){
is Ble.Accelerometer.HistoryPoint.Acceleration, is Ble.Accelerometer.HistoryPoint.Acceleration,
is Ble.Accelerometer.HistoryPoint.Angle -> { is Ble.Accelerometer.HistoryPoint.Angle -> {

View File

@ -3,40 +3,42 @@ package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerContract { class AccelerometerContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnShowChart : Event() data object OnNavigateUp : Event()
data object OnRestart : Event()
data object OnShowAccelerometerHistory : Event() data object OnShowAccelerometerHistory : Event()
data object OnShowRealtimeForm : Event() data object OnShowRealtimeForm : Event()
data object OnPowerEdit : Event()
data object OnShowWriteBlePreview : Event()
data object OnWriteBle : Event() data object OnWriteBle : Event()
data object OnChangePassword : Event() data object OnChangePassword : Event()
data object OnSaveIntervalEdit : Event() data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data object OnReadIntervalEdit : Event() data object OnReadIntervalEdit : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
data object OnPowerEdit : Event()
data class OnPowerChanged( data class OnPowerChanged(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Event() ) : Event()
data object OnShowHistoryForm : Event() data object OnShowHistoryForm : Event()
@ -48,59 +50,30 @@ class AccelerometerContract {
val scale: AccelScale val scale: AccelScale
) : Event() ) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
} }
sealed class State : ViewState { sealed class State : ViewState {
data object Loading : State() data class Loading(
val attempt: Int?
) : State()
data class Display( data class Display(
val origin: Ble.Accelerometer, val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer, val accelerometer: Ble.Accelerometer
val writeState: WriteState?, ) : State()
val accelViewMode: AccelViewMode,
val accelRealtimeViewMode: RealtimeViewMode,
val accelScale: AccelScale,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
} }
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
object ShowWriteBle : Effect()
sealed class Navigation : Effect() { sealed class Navigation : Effect() {
data class Write(
val serial: String,
val request: Ble.Accelerometer.WriteRequest
) : Effect()
data object ShowRealtimeForm : Effect() data object ShowRealtimeForm : Effect()
data object ShowHistoryForm : Effect() data object ShowHistoryForm : Effect()
@ -114,7 +87,7 @@ class AccelerometerContract {
) : Navigation() ) : Navigation()
data class TxPowerSelector( data class TxPowerSelector(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Navigation() ) : Navigation()
data class ChangePassword( data class ChangePassword(
@ -122,14 +95,11 @@ class AccelerometerContract {
) : Navigation() ) : Navigation()
data class AccelHistory( data class AccelHistory(
val ble: BleInfo, val serial: String
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Navigation() ) : Navigation()
data object Up : Navigation()
} }
} }

View File

@ -2,23 +2,17 @@ package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -26,30 +20,23 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryFormDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryFormDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeFormDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeFormDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form.AccelerometerHistoryFormData import llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form.AccelerometerHistoryFormData
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.DisplayState import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.LoadingState import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph> @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -57,19 +44,14 @@ fun AccelerometerScreen(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
bleSerial: String, bleSerial: String,
historyFormResult: ResultRecipient<AccelerometerHistoryFormDestination, AccelerometerHistoryFormData>, historyFormResult: ResultRecipient<AccelerometerHistoryFormDestination, AccelerometerHistoryFormData>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>, txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>, readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
writeResult: ResultRecipient<AccelerometerWriteScreenDestination, Boolean>,
) { ) {
val viewModel = hiltViewModel<AccelerometerViewModel>() val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
historyFormResult.onResult { historyFormResult.onResult {
viewModel.setEvent(AccelerometerContract.Event.OnEnableSaveHistory(it.mode, it.scale)) viewModel.setEvent(AccelerometerContract.Event.OnEnableSaveHistory(it.mode, it.scale))
} }
@ -90,65 +72,16 @@ fun AccelerometerScreen(
} }
LaunchedEffect( writeResult.onResult {
key1 = bottomDialog.sheetState?.currentValue, if(it) viewModel.setEvent(AccelerometerContract.Event.OnRestart)
block = {
if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
bottomDialog.setContent {}
sheetPage = null
}
}
)
val scope = rememberCoroutineScope()
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
} }
} LaunchedEffect(Unit){
}
null -> {
bottomDialog.hide()
}
}
}
DisposableEffect(Unit){
onDispose {
scope.launch {
bottomDialog.hide()
}
}
}
LaunchedEffect("effect"){
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
is AccelerometerContract.Effect.ShowWriteBle -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.Navigation.AccelHistory -> is AccelerometerContract.Effect.Navigation.AccelHistory ->
navigator.navigate(AccelerometerHistoryDestination(it.ble.serial)) navigator.navigate(AccelerometerHistoryDestination(it.serial))
is AccelerometerContract.Effect.Navigation.ChangePassword -> is AccelerometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.serial)) navigator.navigate(ChangePasswordScreenDestination(it.serial))
@ -156,13 +89,14 @@ fun AccelerometerScreen(
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector -> is AccelerometerContract.Effect.Navigation.ReadIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination( navigator.navigate(DurationSelectorScreenDestination(
qualifier = "ReadIntervalSelector", qualifier = "ReadIntervalSelector",
duration = it.interval duration = it.interval,
minimum = 1000
)) ))
is AccelerometerContract.Effect.Navigation.SaveIntervalSelector -> is AccelerometerContract.Effect.Navigation.SaveIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination( navigator.navigate(DurationSelectorScreenDestination(
qualifier = "SaveIntervalSelector", qualifier = "SaveIntervalSelector",
duration = it.interval duration = it.interval,
)) ))
is AccelerometerContract.Effect.Navigation.TxPowerSelector -> is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
@ -173,6 +107,15 @@ fun AccelerometerScreen(
AccelerometerContract.Effect.Navigation.ShowRealtimeForm -> AccelerometerContract.Effect.Navigation.ShowRealtimeForm ->
navigator.navigate(AccelerometerRealtimeFormDestination(bleSerial)) navigator.navigate(AccelerometerRealtimeFormDestination(bleSerial))
is AccelerometerContract.Effect.Navigation.Write ->
navigator.navigate(AccelerometerWriteScreenDestination(
bleSerial = it.serial,
writeRequest = it.request
))
AccelerometerContract.Effect.Navigation.Up ->
navigator.navigateUp()
} }
}.launchIn(this) }.launchIn(this)
} }
@ -194,6 +137,20 @@ fun AccelerometerScreen(
}, },
title = { title = {
Text(text = BleInfo.Type.ACCELEROMETER.localized) Text(text = BleInfo.Type.ACCELEROMETER.localized)
},
actions = {
if(state is AccelerometerContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(AccelerometerContract.Event.OnRestart)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
} }
) )
} }
@ -205,14 +162,12 @@ fun AccelerometerScreen(
when(state){ when(state){
is AccelerometerContract.State.Display -> { is AccelerometerContract.State.Display -> {
DisplayState( DisplayState(viewModel, state)
origin = state.origin, }
ble = state.accelerometer, is AccelerometerContract.State.Loading -> LoadingState(
onEvent = viewModel::setEvent viewModel, state
) )
} }
is AccelerometerContract.State.Loading -> LoadingState()
}
} }

View File

@ -4,18 +4,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject import javax.inject.Inject
@ -23,66 +17,21 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AccelerometerViewModel @Inject constructor( class AccelerometerViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial, private val getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() { ) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
init { init {
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle) loadData()
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
if(ble != null && ble is Ble.Accelerometer){
setState {
when(this){
is AccelerometerContract.State.Display -> {
copy(
origin = Ble.Accelerometer(
info = ble.info,
state = origin.state,
accelerometerState = origin.accelerometerState
)
)
}
AccelerometerContract.State.Loading -> {
AccelerometerContract.State.Display(
origin = ble,
accelerometer = bleMapper.map(ble) as BleView.Accelerometer,
writeState = null,
accelViewMode = AccelViewMode.ACCELERATION,
accelRealtimeViewMode = RealtimeViewMode.Accel(
AccelViewMode.ACCELERATION
),
fftAxis = FftAxis.AUTO,
fftFrequency = FftFrequency.F_400,
fftViewMode = FftViewMode.SPECTRE,
accelScale = AccelScale.S_2
)
}
}
}
}
} }
} override fun setInitialState() = AccelerometerContract.State.Loading(null)
override fun setInitialState() = AccelerometerContract.State.Loading
override fun handleEvents(event: AccelerometerContract.Event) { override fun handleEvents(event: AccelerometerContract.Event) {
when(event){ when(event){
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event) is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event) is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event) is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnDisableSaveHistory -> reduce(viewState.value, event) is AccelerometerContract.Event.OnDisableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event) is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
@ -93,22 +42,32 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event) is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnEnableSaveHistory -> reduce(viewState.value, event) is AccelerometerContract.Event.OnEnableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowHistoryForm -> reduce(viewState.value, event) is AccelerometerContract.Event.OnShowHistoryForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowChart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowRealtimeForm -> reduce(viewState.value, event) is AccelerometerContract.Event.OnShowRealtimeForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnRestart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
} }
} }
private fun reduce( private fun reduce(
state: AccelerometerContract.State, state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowChart event: AccelerometerContract.Event.OnNavigateUp
) { ) {
setEffect { setEffect {
AccelerometerContract.Effect.Navigation.ShowHistoryForm AccelerometerContract.Effect.Navigation.Up
} }
} }
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnRestart
) {
loadData()
}
private fun reduce( private fun reduce(
state: AccelerometerContract.State, state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowRealtimeForm event: AccelerometerContract.Event.OnShowRealtimeForm
@ -138,7 +97,15 @@ class AccelerometerViewModel @Inject constructor(
if(state is AccelerometerContract.State.Display) { if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.readInterval = event.interval setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
readInterval = event.interval
)
)
)
}
} }
@ -184,7 +151,15 @@ class AccelerometerViewModel @Inject constructor(
if(state is AccelerometerContract.State.Display) { if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.historyInterval = event.interval setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
historyInterval = event.interval
)
)
)
}
} }
@ -212,7 +187,15 @@ class AccelerometerViewModel @Inject constructor(
if(state is AccelerometerContract.State.Display) { if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Disabled setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
saveHistorySettings = Ble.Accelerometer.HistorySettings.Disabled
)
)
)
}
} }
@ -225,11 +208,19 @@ class AccelerometerViewModel @Inject constructor(
if(state is AccelerometerContract.State.Display) { if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Enabled( setState {
state.copy(
accelerometer = state.accelerometer.copy(
accelerometerState = state.accelerometer.accelerometerState.copy(
saveHistorySettings = Ble.Accelerometer.HistorySettings.Enabled(
scale = event.scale, scale = event.scale,
mode = event.mode, mode = event.mode,
detailed = true detailed = true
) )
)
)
)
}
} }
@ -246,12 +237,7 @@ class AccelerometerViewModel @Inject constructor(
setEffect { setEffect {
AccelerometerContract.Effect.Navigation.AccelHistory( AccelerometerContract.Effect.Navigation.AccelHistory(
ble = state.accelerometer.info, serial = state.accelerometer.info.serial
accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
) )
} }
@ -261,12 +247,12 @@ class AccelerometerViewModel @Inject constructor(
private fun reduce( private fun reduce(
state: AccelerometerContract.State, state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview event: AccelerometerContract.Event.OnWriteBle
) { ) {
if(state is AccelerometerContract.State.Display){ if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer val newBle = state.accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest( val writeRequest = Ble.Accelerometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx, tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
@ -275,16 +261,8 @@ class AccelerometerViewModel @Inject constructor(
readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval, readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval,
) )
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect { setEffect {
AccelerometerContract.Effect.ShowWriteBle AccelerometerContract.Effect.Navigation.Write(state.accelerometer.info.serial, writeRequest)
} }
} }
@ -298,7 +276,15 @@ class AccelerometerViewModel @Inject constructor(
if(state is AccelerometerContract.State.Display) { if(state is AccelerometerContract.State.Display) {
state.accelerometer.state.tx = event.tx setState {
state.copy(
accelerometer = state.accelerometer.copy(
state = state.accelerometer.state.copy(
tx = event.tx
)
)
)
}
} }
@ -318,68 +304,50 @@ class AccelerometerViewModel @Inject constructor(
} }
} }
private fun reduce( private var loadJob: Job? = null
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle private fun loadData(){
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState {
AccelerometerContract.State.Loading(null)
}
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
AccelerometerContract.State.Loading(it)
}
}
){ ){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if(state is AccelerometerContract.State.Display){ if( ble is Ble.Accelerometer){
state.writeState?.let { request ->
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState { setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing( when(this){
request.writeRequest is AccelerometerContract.State.Display -> {
copy(
origin = Ble.Accelerometer(
info = ble.info,
state = origin.state,
accelerometerState = origin.accelerometerState
) )
) )
} }
is AccelerometerContract.State.Loading -> {
writeBle(state.accelerometer.info.serial, request.writeRequest).fold( AccelerometerContract.State.Display(
onSuccess = { origin = ble,
accelerometer = ble
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistorySettings = request.writeRequest.saveHistorySettings
?: currentState.origin.accelerometerState.saveHistorySettings
)
)
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
) )
} }
} }
) }
}
}
} }
} }

View File

@ -1,47 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftAxis
@Composable
fun SelectorItem(
label: String,
selected: Boolean,
onClick: () -> Unit
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onClick)
.padding(4.dp)
) {
RadioButton(
selected = selected,
onClick = onClick
)
Text(text = label)
}
}

View File

@ -1,29 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
@Serializable @Serializable

View File

@ -1,28 +1,31 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.background import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerViewModel
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.app.ui.screen.locale.value
import llc.arma.ble.data.repository.BleRepositoryImpl import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -30,9 +33,8 @@ import kotlin.time.toDuration
@Composable @Composable
fun DisplayState( fun DisplayState(
onEvent: (AccelerometerContract.Event) -> Unit, viewModel: AccelerometerViewModel,
origin: Ble.Accelerometer, state: AccelerometerContract.State.Display
ble: BleView.Accelerometer
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@ -48,8 +50,8 @@ fun DisplayState(
) { ) {
BleInfoView( BleInfoView(
bleInfo = origin.info, bleInfo = state.origin.info,
version = origin.state.version version = state.origin.state.version
) )
Column( Column(
@ -59,7 +61,7 @@ fun DisplayState(
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Start, shapeType = ShapeType.Start,
title = "Мощность", title = "Мощность",
subtitle = "${ble.state.tx.value} db", subtitle = "${state.accelerometer.state.tx.value} db",
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.KeyboardArrowDown, imageVector = Icons.Rounded.KeyboardArrowDown,
@ -67,10 +69,10 @@ fun DisplayState(
) )
} }
) { ) {
onEvent(AccelerometerContract.Event.OnPowerEdit) viewModel.setEvent(AccelerometerContract.Event.OnPowerEdit)
} }
val history = ble.accelerometerState.saveHistory val history = state.accelerometer.accelerometerState.saveHistorySettings
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
@ -82,12 +84,12 @@ fun DisplayState(
}, },
icon = { icon = {
Switch( Switch(
checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled, checked = state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled,
onCheckedChange = { onCheckedChange = {
if(it){ if(it){
onEvent(AccelerometerContract.Event.OnShowHistoryForm) viewModel.setEvent(AccelerometerContract.Event.OnShowHistoryForm)
} else { } else {
onEvent(AccelerometerContract.Event.OnDisableSaveHistory) viewModel.setEvent(AccelerometerContract.Event.OnDisableSaveHistory)
} }
} }
@ -95,12 +97,12 @@ fun DisplayState(
} }
) )
if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) { if (state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Интервал измерений", title = "Интервал измерений",
subtitle = ble.accelerometerState.historyInterval subtitle = state.accelerometer.accelerometerState.historyInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ -> .toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." }, "$hours ч. $minutes мин. $seconds сек." },
icon = { icon = {
@ -111,20 +113,20 @@ fun DisplayState(
} }
) { ) {
onEvent(AccelerometerContract.Event.OnSaveIntervalEdit) viewModel.setEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
} }
} }
if (ble.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) { if (state.accelerometer.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) { if (state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Интервал чтения", title = "Интервал чтения",
subtitle = ble.accelerometerState.readInterval subtitle = state.accelerometer.accelerometerState.readInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ -> .toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." }, "$hours ч. $minutes мин. $seconds сек." },
icon = { icon = {
@ -135,7 +137,7 @@ fun DisplayState(
} }
) { ) {
onEvent(AccelerometerContract.Event.OnReadIntervalEdit) viewModel.setEvent(AccelerometerContract.Event.OnReadIntervalEdit)
} }
@ -154,12 +156,12 @@ fun DisplayState(
} }
) { ) {
when (origin.accelerometerState.saveHistorySettings) { when (state.origin.accelerometerState.saveHistorySettings) {
is Ble.Accelerometer.HistorySettings.Disabled -> is Ble.Accelerometer.HistorySettings.Disabled ->
onEvent(AccelerometerContract.Event.OnShowRealtimeForm) viewModel.setEvent(AccelerometerContract.Event.OnShowRealtimeForm)
is Ble.Accelerometer.HistorySettings.Enabled -> is Ble.Accelerometer.HistorySettings.Enabled ->
onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory) viewModel.setEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
} }
} }
@ -174,25 +176,36 @@ fun DisplayState(
) )
} }
) { ) {
onEvent(AccelerometerContract.Event.OnChangePassword) viewModel.setEvent(AccelerometerContract.Event.OnChangePassword)
} }
} }
} }
PrimaryButton( Box(
modifier = Modifier.shadow( modifier = Modifier.fillMaxWidth().animateContentSize()
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) { ) {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview) if(state.origin != state.accelerometer) {
Button(
onClick = {
viewModel.setEvent(AccelerometerContract.Event.OnWriteBle)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
} }

View File

@ -2,23 +2,30 @@ package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun LoadingState(){ fun LoadingState(
viewModel: AccelerometerViewModel,
state: AccelerometerContract.State.Loading
){
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
ContainedLoadingIndicator() RetryingLoadingTemplate(state.attempt) {
viewModel.setEvent(AccelerometerContract.Event.OnNavigateUp)
}
} }

View File

@ -1,262 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localizedName
import llc.arma.ble.domain.model.Ble
@Composable
fun Write(
state: AccelerometerContract.State.Display.WriteState,
onEvent: (AccelerometerContract.Event) -> Unit
) {
Column(
modifier = Modifier.animateContentSize()
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is AccelerometerContract.State.Display.WriteState.DisplayPreview -> {
if(
state.writeRequest.tx != null ||
state.writeRequest.saveHistorySettings != null ||
state.writeRequest.historyInterval != null ||
state.writeRequest.readInterval != null
) {
state.writeRequest.tx?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db",
)
}
state.writeRequest.saveHistorySettings?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
is Ble.Accelerometer.HistorySettings.Enabled -> "Включено"
},
)
}
state.writeRequest.historyInterval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
}
state.writeRequest.readInterval?.let {
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
}
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Записать"
) {
onEvent(AccelerometerContract.Event.OnWriteBle)
}
SecondaryButton(
label = "Отменить"
) {
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
PrimaryButton(
label = "Ок"
) {
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
}
is AccelerometerContract.State.Display.WriteState.Writing -> {
Box {
Column {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
SecondaryButton(
label = "Отменить"
) {
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
AccelerometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(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))
PrimaryButton(
label = "Ок"
) {
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
AccelerometerContract.State.Display.WriteState.Failure -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Ок"
) {
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
}
}
}

View File

@ -4,33 +4,54 @@ import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerAccelContract { class AccelerometerAccelContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object OnRefresh : Event() data object OnRefresh : Event()
} }
sealed class State : ViewState { sealed class State : ViewState {
data class Display( data class Loading(
val mode: AccelViewMode, val attempt: Int?
val measureHistory : List<Ble.Accelerometer.RealtimePoint>
) : State() ) : State()
data object Exception : State() data class DisplayCommon(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Common>
) : State()
data class DisplayAngle(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Angle>
) : State()
data class DisplayRotation(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Rotation>
) : State()
data class DisplayVibration(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint.Vibration>
) : State()
} }
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
}
} }
} }

View File

@ -9,11 +9,11 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import javax.inject.Inject import javax.inject.Inject
@ -26,17 +26,15 @@ class AccelerometerAccelViewModel @Inject constructor(
private var measureJob: Job? = null private var measureJob: Job? = null
init { init {
startReadMeasure(false) startReadMeasure()
} }
override fun setInitialState() = AccelerometerAccelContract.State.Display( override fun setInitialState() = AccelerometerAccelContract.State.Loading(null)
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
override fun handleEvents(event: AccelerometerAccelContract.Event) { override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){ when(event){
is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event) is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnNavigateUp -> reduce(viewState.value, event)
} }
} }
@ -44,26 +42,36 @@ class AccelerometerAccelViewModel @Inject constructor(
state: AccelerometerAccelContract.State, state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefresh event: AccelerometerAccelContract.Event.OnRefresh
) { ) {
startReadMeasure(true) startReadMeasure()
} }
private fun startReadMeasure( private fun reduce(
restartJob: Boolean state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnNavigateUp
) { ) {
setEffect { AccelerometerAccelContract.Effect.Navigation.Up }
}
private fun startReadMeasure() {
val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle) val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle)
if(restartJob || measureJob == null) { setState {
AccelerometerAccelContract.State.Loading(null)
}
measureJob?.cancel() measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch { measureJob = viewModelScope.launch {
val flow = retryUntilNotNull(
onNewAttempt = {
setState { setState {
AccelerometerAccelContract.State.Display( AccelerometerAccelContract.State.Loading(it)
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
} }
}
) {
getAccelerometerMeasureBySerialFlow( getAccelerometerMeasureBySerialFlow(
params.bleSerial, params.bleSerial,
@ -71,44 +79,73 @@ class AccelerometerAccelViewModel @Inject constructor(
params.accelMode, params.accelMode,
params.fftAxis, params.fftAxis,
params.fftMode, params.fftMode,
params.frequency FftFrequency.F_400
).onEach { ).getOrNull()
it.fold(
onSuccess = {
setState {
when (this) {
is AccelerometerAccelContract.State.Display -> {
var dataList = this.measureHistory.toMutableList().apply {
add(it)
}
if(params.accelMode != AccelViewMode.ANGLE) {
dataList = dataList.takeLast(100).toMutableList()
}
AccelerometerAccelContract.State.Display(
params.accelMode,
dataList
)
} }
AccelerometerAccelContract.State.Exception -> { flow.onEach {
AccelerometerAccelContract.State.Display(
val state = viewState.value
val newState = when (it) {
is Ble.Accelerometer.RealtimePoint.Angle -> {
if (state is AccelerometerAccelContract.State.DisplayAngle) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayAngle(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Common -> {
if (state is AccelerometerAccelContract.State.DisplayCommon) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayCommon(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Rotation -> {
if (state is AccelerometerAccelContract.State.DisplayRotation) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayRotation(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Vibration -> {
if (state is AccelerometerAccelContract.State.DisplayVibration) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayVibration(
params.accelMode, params.accelMode,
listOf(it) listOf(it)
) )
} }
} }
} }
}, setState { newState }
onFailure = {
setState {
AccelerometerAccelContract.State.Exception
}
}
)
}.launchIn(this) }.launchIn(this)
} }
}
} }
} }

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import android.graphics.Color
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
@ -26,7 +28,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
@ -44,6 +48,8 @@ import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.component.shape.ShapeComponent
import com.patrykandpatrick.vico.core.component.text.TextComponent
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
@ -51,9 +57,10 @@ import com.patrykandpatrick.vico.core.scroll.InitialScroll
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis
@ -76,6 +83,15 @@ fun AccelerometerRealtime(
val viewModel = hiltViewModel<AccelerometerAccelViewModel>() val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when (it) {
AccelerometerAccelContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -96,6 +112,9 @@ fun AccelerometerRealtime(
) )
}, },
actions = { actions = {
if((state is AccelerometerAccelContract.State.Loading).not()) {
IconButton( IconButton(
onClick = { onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh) viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
@ -109,14 +128,21 @@ fun AccelerometerRealtime(
} }
} }
}
) )
} }
) { ) {
Box(modifier = Modifier.padding(it)) { Box(
modifier = Modifier.padding(it)
) {
when (state) { when (state) {
is AccelerometerAccelContract.State.Display -> DisplayState(state = state) is AccelerometerAccelContract.State.DisplayAngle -> DisplayAngleState(state)
is AccelerometerAccelContract.State.Exception -> ExceptionState() is AccelerometerAccelContract.State.DisplayCommon -> DisplayCommonState(state)
is AccelerometerAccelContract.State.DisplayRotation -> DisplayRotationState(state)
is AccelerometerAccelContract.State.DisplayVibration -> DisplayVibrationState(state)
is AccelerometerAccelContract.State.Loading -> LoadingState(viewModel, state)
} }
} }
@ -124,10 +150,188 @@ fun AccelerometerRealtime(
} }
@Composable @Composable
private fun DisplayState( private fun DisplayCommonState(
state: AccelerometerAccelContract.State.Display state: AccelerometerAccelContract.State.DisplayCommon
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.fillMaxSize()
) {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.x)
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.y)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.z)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
lineComponent = ShapeComponent(color = Color.TRANSPARENT),
thresholdValue = 0f
)
),
persistentMarkers = mapOf(
xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Surface(
shape = ShapeType.Start.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = 8.dp)
) {
Text(
text = "Ось X",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.Middle.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = "Ось Y",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.End.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(bottom = 8.dp)
) {
Text(
text = "Ось Z",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
@Composable
private fun DisplayAngleState(
state: AccelerometerAccelContract.State.DisplayAngle
) { ) {
Box(modifier = Modifier Box(modifier = Modifier
@ -159,68 +363,20 @@ private fun DisplayState(
} }
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.x ) FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.angle )
}
}) })
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.y) FloatEntry(index.toFloat(), measurePoint.y)
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.y)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.tmp)
}
}) })
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint -> zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.z) FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.turnovers.toFloat())
}
}) })
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
),
persistentMarkers = mapOf(xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val marker = MarkerComponent( val lastMeasure = state.measureHistory.last()
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
val lastMeasure = state.measureHistory.lastOrNull()
when(lastMeasure){
is Ble.Accelerometer.RealtimePoint.Angle -> {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@ -275,8 +431,78 @@ private fun DisplayState(
} }
} }
} }
is Ble.Accelerometer.RealtimePoint.Rotation -> {
}
}
@Composable
private fun DisplayRotationState(
state: AccelerometerAccelContract.State.DisplayRotation
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
strokeCap = StrokeCap.Round
)
} else {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.angle )
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.tmp)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.turnovers.toFloat())
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
),
persistentMarkers = mapOf(xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
val lastMeasure = state.measureHistory.last()
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
@ -337,11 +563,54 @@ private fun DisplayState(
) )
} }
}
is Ble.Accelerometer.RealtimePoint.Vibration -> {
Column {
Text(text = "Вибрация:") }
}
}
@Composable
private fun DisplayVibrationState(
state: AccelerometerAccelContract.State.DisplayVibration
) {
Box(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.value)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
labelComponent = textComponent(color = androidx.compose.ui.graphics.Color.Transparent),
lineComponent = ShapeComponent(color = Color.TRANSPARENT),
thresholdValue = 0f
)
),
persistentMarkers = mapOf(
xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Chart( Chart(
marker = marker, marker = marker,
@ -349,9 +618,7 @@ private fun DisplayState(
chartModelProducer = xProducer, chartModelProducer = xProducer,
startAxis = startAxis(), startAxis = startAxis(),
bottomAxis = bottomAxis(), bottomAxis = bottomAxis(),
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None, autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0), diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec( chartScrollSpec = rememberChartScrollSpec(
@ -361,78 +628,10 @@ private fun DisplayState(
) )
) )
}
}
is Ble.Accelerometer.RealtimePoint.Common -> {
Column {
Text(text = "Ось X:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Y:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Z:")
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
null -> {}
}
} }
} }
}
@Composable @Composable
fun Angle( fun Angle(
@ -504,21 +703,21 @@ fun Angle(
} }
@Composable @Composable
private fun ExceptionState( private fun LoadingState(
viewModel: AccelerometerAccelViewModel,
state: AccelerometerAccelContract.State.Loading
) { ) {
Box( Box(
contentAlignment = Alignment.Center,
modifier = Modifier modifier = Modifier
.padding(8.dp) .fillMaxSize(),
.fillMaxWidth()
.aspectRatio(2f),
){ ){
Text( RetryingLoadingTemplate(
textAlign = TextAlign.Center, attempt = state.attempt
text = "Во время загрузки произошла ошибка", ) {
modifier = Modifier.align(Alignment.Center) viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
) }
} }

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt.form
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -9,15 +9,12 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -36,16 +33,10 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.filter.BleFilterContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis

View File

@ -4,14 +4,10 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -25,7 +21,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
@ -50,7 +49,6 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis

View File

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

View File

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

View File

@ -3,7 +3,6 @@ package llc.arma.ble.app.ui.screen.inspection.beacon
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract.Effect.Navigation import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract.Effect.Navigation
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
@ -13,11 +12,7 @@ class BeaconContract {
data object OnNavigateUp : Event() data object OnNavigateUp : Event()
object OnWriteBle : Event() data object OnShowWriteBlePreview : Event()
object OnHideWriteBlePreview : Event()
object OnShowWriteBlePreview : Event()
data object OnPowerEdit : Event() data object OnPowerEdit : Event()
@ -25,56 +20,36 @@ class BeaconContract {
val ble: Ble.Beacon val ble: Ble.Beacon
) : Event() ) : Event()
data class OnPowerChanged( data class OnTxChanged(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Event() ) : Event()
data class OnTxChanged(val tx: BleView.BleState.TX) : Event() data object OnChangePassword : Event()
object OnNavigateUpClicked : Event()
object OnChangePassword : Event()
} }
sealed class State : ViewState { sealed class State : ViewState {
data object Loading : State() data class Loading(
val attempt: Int?
) : State()
data class Display( data class Display(
val origin: Ble.Beacon, val origin: Ble.Beacon,
val beacon: BleView.Beacon, val beacon: Ble.Beacon
val writeState: WriteState? ) : State()
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
} }
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
data object HideWriteBlePreview : Effect()
data object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() { sealed class Navigation : Effect() {
data class Write(
val bleSerial: String,
val writeRequest: Ble.Beacon.WriteRequest
) : Navigation()
data object Up : Navigation() data object Up : Navigation()
data class PasswordForm( data class PasswordForm(
@ -82,7 +57,7 @@ class BeaconContract {
) : Navigation() ) : Navigation()
data class TxSelector( data class TxSelector(
val tx: BleView.BleState.TX? val tx: Ble.BleState.TX?
) : Navigation() ) : Navigation()
} }

View File

@ -1,12 +1,10 @@
package llc.arma.ble.app.ui.screen.inspection.beacon package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@ -17,53 +15,39 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.BeaconWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph> @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BeaconScreen( fun BeaconScreen(
bleSerial: String, bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>, txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
navigator: DestinationsNavigator navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<BeaconViewModel>() val viewModel = hiltViewModel<BeaconViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult { txSelectResult.onResult {
viewModel.setEvent(BeaconContract.Event.OnTxChanged(it)) viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
} }
@ -72,15 +56,6 @@ fun BeaconScreen(
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
BeaconContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is BeaconContract.Effect.Navigation.PasswordForm -> is BeaconContract.Effect.Navigation.PasswordForm ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial)) navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
@ -88,35 +63,14 @@ fun BeaconScreen(
navigator.navigate(TxPowerSelectorScreenDestination(it.tx)) navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
BeaconContract.Effect.Navigation.Up -> BeaconContract.Effect.Navigation.Up ->
navigator.popBackStack() navigator.navigateUp()
is BeaconContract.Effect.Navigation.Write ->
navigator.navigate(BeaconWriteScreenDestination(it.bleSerial, it.writeRequest))
} }
}.launchIn(this) }.launchIn(this)
} }
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is BeaconContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -144,34 +98,30 @@ fun BeaconScreen(
) { ) {
when(state){ when(state){
is BeaconContract.State.Display -> DisplayState( is BeaconContract.State.Display -> DisplayState(viewModel, state)
onEvent = { is BeaconContract.State.Loading -> LoadingState(viewModel, state)
viewModel.setEvent(it)
},
ble = state.beacon,
origin = state.origin
)
is BeaconContract.State.Loading -> LoadingState()
} }
} }
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun LoadingState(){ private fun LoadingState(
viewModel: BeaconViewModel,
state: BeaconContract.State.Loading,
){
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
){ ){
ContainedLoadingIndicator() RetryingLoadingTemplate(state.attempt){
viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
}
} }

View File

@ -2,22 +2,14 @@ package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject import javax.inject.Inject
@ -26,9 +18,6 @@ import javax.inject.Inject
class BeaconViewModel @Inject constructor( class BeaconViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial, getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() { ) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
init { init {
@ -37,12 +26,17 @@ class BeaconViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold( val ble = retryUntilNotNull(
onSuccess = { it }, onNewAttempt = {
onFailure = { null } setState {
) BeaconContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if(ble != null && ble is Ble.Beacon){ if(ble is Ble.Beacon){
setState { setState {
when(this){ when(this){
@ -54,11 +48,10 @@ class BeaconViewModel @Inject constructor(
) )
) )
} }
BeaconContract.State.Loading -> { is BeaconContract.State.Loading -> {
BeaconContract.State.Display( BeaconContract.State.Display(
origin = ble, origin = ble,
beacon = bleMapper.map(ble) as BleView.Beacon, beacon = ble
writeState = null
) )
} }
} }
@ -69,36 +62,19 @@ class BeaconViewModel @Inject constructor(
} }
override fun setInitialState() = BeaconContract.State.Loading override fun setInitialState() = BeaconContract.State.Loading(null)
override fun handleEvents(event: BeaconContract.Event) { override fun handleEvents(event: BeaconContract.Event) {
when(event){ when(event){
is BeaconContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event) is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnBleChanged -> reduce(viewState.value, event) is BeaconContract.Event.OnBleChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnChangePassword -> reduce(viewState.value, event) is BeaconContract.Event.OnChangePassword -> reduce(viewState.value, event)
is BeaconContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is BeaconContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event) is BeaconContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is BeaconContract.Event.OnWriteBle -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event) is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is BeaconContract.Event.OnNavigateUp -> reduce(viewState.value, event) is BeaconContract.Event.OnNavigateUp -> reduce(viewState.value, event)
} }
} }
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnPowerChanged
) {
if(state is BeaconContract.State.Display) {
state.beacon.state.tx = event.tx
}
}
private fun reduce( private fun reduce(
state: BeaconContract.State, state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUp event: BeaconContract.Event.OnNavigateUp
@ -124,19 +100,21 @@ class BeaconViewModel @Inject constructor(
} }
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked
) {
setEffect { BeaconContract.Effect.Navigation.Up }
}
private fun reduce( private fun reduce(
state: BeaconContract.State, state: BeaconContract.State,
event: BeaconContract.Event.OnTxChanged event: BeaconContract.Event.OnTxChanged
) { ) {
if(state is BeaconContract.State.Display){
setState {
state.copy(
beacon = state.beacon.copy(
state = state.beacon.state.copy(
tx = event.tx
)
)
)
}
}
} }
private fun reduce( private fun reduce(
@ -159,8 +137,7 @@ class BeaconViewModel @Inject constructor(
setState { setState {
BeaconContract.State.Display( BeaconContract.State.Display(
origin = event.ble, origin = event.ble,
beacon = bleMapper.map(event.ble) as BleView.Beacon, beacon = event.ble
writeState = null
) )
} }
} }
@ -181,15 +158,6 @@ class BeaconViewModel @Inject constructor(
} }
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnHideWriteBlePreview
) {
setEffect {
BeaconContract.Effect.HideWriteBlePreview
}
}
private fun reduce( private fun reduce(
state: BeaconContract.State, state: BeaconContract.State,
event: BeaconContract.Event.OnShowWriteBlePreview event: BeaconContract.Event.OnShowWriteBlePreview
@ -197,83 +165,16 @@ class BeaconViewModel @Inject constructor(
if(state is BeaconContract.State.Display){ if(state is BeaconContract.State.Display){
val newBle = bleViewMapper.map(state.beacon) as Ble.Beacon val params = BeaconScreenDestination.argsFrom(savedStateHandle)
val newBle = state.beacon
val writeRequest = Ble.Beacon.WriteRequest( val writeRequest = Ble.Beacon.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx
) )
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect { setEffect {
BeaconContract.Effect.ShowWriteBlePreview BeaconContract.Effect.Navigation.Write(params.bleSerial, writeRequest)
}
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnWriteBle
) {
if(state is BeaconContract.State.Display){
state.writeState?.let { request ->
if(request is BeaconContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is BeaconContract.State.Display) {
val newBleObject = Ble.Beacon(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
)
)
writeBle(state.beacon.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
beacon = bleMapper.map(newBleObject) as BleView.Beacon,
writeState = BeaconContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
} }
} }

View File

@ -1,49 +1,49 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.view package llc.arma.ble.app.ui.screen.inspection.beacon.view
import androidx.compose.foundation.background import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconViewModel
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.domain.model.Ble import llc.arma.ble.app.ui.screen.locale.value
@Composable @Composable
fun DisplayState( fun DisplayState(
onEvent: (BeaconContract.Event) -> Unit, viewModel: BeaconViewModel,
origin: Ble.Beacon, state: BeaconContract.State.Display
ble: BleView.Beacon
) { ) {
Column {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
BleInfoView( BleInfoView(
bleInfo = origin.info, bleInfo = state.origin.info,
version = origin.state.version version = state.origin.state.version
) )
Column( Column(
@ -53,7 +53,7 @@ fun DisplayState(
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Start, shapeType = ShapeType.Start,
title = "Мощность", title = "Мощность",
subtitle = "${ble.state.tx.value} db", subtitle = "${state.beacon.state.tx.value} db",
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.KeyboardArrowDown, imageVector = Icons.Rounded.KeyboardArrowDown,
@ -61,7 +61,7 @@ fun DisplayState(
) )
} }
) { ) {
onEvent(BeaconContract.Event.OnPowerEdit) viewModel.setEvent(BeaconContract.Event.OnPowerEdit)
} }
BleMenuItem( BleMenuItem(
@ -75,16 +75,27 @@ fun DisplayState(
} }
) { ) {
onEvent(BeaconContract.Event.OnChangePassword) viewModel.setEvent(BeaconContract.Event.OnChangePassword)
} }
} }
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.beacon) {
Button( Button(
onClick = { onClick = {
onEvent(BeaconContract.Event.OnShowWriteBlePreview) viewModel.setEvent(BeaconContract.Event.OnShowWriteBlePreview)
}, },
modifier = Modifier.fillMaxWidth() modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) { ) {
Text( Text(
@ -96,3 +107,7 @@ fun DisplayState(
} }
} }
}
}

View File

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

View File

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

View File

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

View File

@ -37,8 +37,13 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.VerticalDivider import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.* 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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@ -2,7 +2,6 @@ package llc.arma.ble.app.ui.screen.inspection.gate.history
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job

View File

@ -3,7 +3,6 @@ package llc.arma.ble.app.ui.screen.inspection.gate.main
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState 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.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
@ -11,16 +10,14 @@ class GateContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnReload : Event()
data object OnWriteBle : Event() data object OnWriteBle : Event()
data object OnHideWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event()
data object OnTxSelect : Event() data object OnTxSelect : Event()
data class OnPowerChanged( data class OnPowerChanged(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Event() ) : Event()
data object OnHistoryIntervalSelect : Event() data object OnHistoryIntervalSelect : Event()
@ -53,7 +50,7 @@ class GateContract {
data class Display( data class Display(
val origin: Ble.Gate, val origin: Ble.Gate,
val gate: BleView.Gate, val gate: Ble.Gate,
val writeState: WriteState? val writeState: WriteState?
) : State() { ) : State() {
@ -79,10 +76,13 @@ class GateContract {
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
data object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() { sealed class Navigation : Effect() {
data class GateWrite(
val serial: String,
val request: Ble.Gate.WriteRequest
) : Navigation()
data class ChangePassword( data class ChangePassword(
val serial: String, val serial: String,
) : Navigation() ) : Navigation()
@ -98,7 +98,7 @@ class GateContract {
) : Navigation() ) : Navigation()
data class TxSelector( data class TxSelector(
val tx: BleView.BleState.TX? val tx: Ble.BleState.TX?
) : Navigation() ) : Navigation()
data class ReadIntervalSelector( data class ReadIntervalSelector(

View File

@ -9,24 +9,19 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -38,25 +33,20 @@ import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScr
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph> @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -64,19 +54,18 @@ enum class SheetPage {
fun GateScreen( fun GateScreen(
bleSerial: String, bleSerial: String,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>, readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>, txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
writeResult: ResultRecipient<GateWriteScreenDestination, Boolean>,
navigator: DestinationsNavigator navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<GateViewModel>() val viewModel = hiltViewModel<GateViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
var sheetPage by rememberSaveable { writeResult.onResult {
mutableStateOf<SheetPage?>(null) if(it) viewModel.setEvent(GateContract.Event.OnReload)
} }
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult { txSelectResult.onResult {
viewModel.setEvent(GateContract.Event.OnPowerChanged(it)) viewModel.setEvent(GateContract.Event.OnPowerChanged(it))
} }
@ -96,11 +85,6 @@ fun GateScreen(
LaunchedEffect(Unit){ LaunchedEffect(Unit){
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
GateContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is GateContract.Effect.Navigation.BleTable -> is GateContract.Effect.Navigation.BleTable ->
navigator.navigate(GateBleTableScreenDestination(it.serial)) navigator.navigate(GateBleTableScreenDestination(it.serial))
@ -131,35 +115,13 @@ fun GateScreen(
maximum = 10 * 24 * 60 * 60 * 1000 maximum = 10 * 24 * 60 * 60 * 1000
)) ))
is GateContract.Effect.Navigation.GateWrite ->
navigator.navigate(GateWriteScreenDestination(it.serial, it.request))
} }
}.launchIn(this) }.launchIn(this)
} }
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is GateContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@ -179,6 +141,20 @@ fun GateScreen(
Text( Text(
text = BleInfo.Type.HOST.localized text = BleInfo.Type.HOST.localized
) )
},
actions = {
if(state is GateContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnReload)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
} }
) )
} }
@ -199,11 +175,10 @@ fun GateScreen(
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun LoadingState( private fun LoadingState(
viewModel: GateViewModel, viewModel: GateViewModel,
state: GateContract.State.Loading state: GateContract.State.Loading,
){ ){
Box( Box(
@ -211,44 +186,9 @@ private fun LoadingState(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
){ ){
Column( RetryingLoadingTemplate(state.attempt){
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.widthIn(max = 230.dp)
) {
ContainedLoadingIndicator()
state.attempt?.let {
Spacer(Modifier.height(16.dp))
Text(
text = "Повторная попытка ${it}"
)
Text(
text = "Во время загрузки произошла ошибка",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
TextButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnNavigateUp) viewModel.setEvent(GateContract.Event.OnNavigateUp)
} }
) {
Text(
text = "Отмена"
)
}
}
}
} }

View File

@ -5,12 +5,11 @@ import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle import llc.arma.ble.domain.usecase.WriteBle
@ -20,68 +19,12 @@ import javax.inject.Inject
class GateViewModel @Inject constructor( class GateViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial, private val getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle, private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<GateContract.State, GateContract.Event, GateContract.Effect>() { ) : BaseViewModel<GateContract.State, GateContract.Event, GateContract.Effect>() {
init { init {
val params = GateScreenDestination.argsFrom(savedStateHandle) loadData()
viewModelScope.launch {
var attempt = 0
var ble: Ble? = null
while (ble == null) {
ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { return@fold it },
onFailure = { return@fold null }
)
if(ble == null) {
setState {
attempt++
GateContract.State.Loading(attempt)
}
}
}
if (ble is Ble.Gate) {
setState {
when (this) {
is GateContract.State.Display -> {
copy(
origin = Ble.Gate(
info = ble.info,
state = origin.state,
gateState = origin.gateState
)
)
}
is GateContract.State.Loading -> {
GateContract.State.Display(
origin = ble,
gate = bleMapper.map(ble) as BleView.Gate,
writeState = null
)
}
}
}
}
}
} }
@ -91,8 +34,6 @@ class GateViewModel @Inject constructor(
when(event){ when(event){
is GateContract.Event.OnNavigateUp -> reduce(viewState.value, event) is GateContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is GateContract.Event.OnChangePassword -> reduce(viewState.value, event) is GateContract.Event.OnChangePassword -> reduce(viewState.value, event)
is GateContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is GateContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is GateContract.Event.OnWriteBle -> reduce(viewState.value, event) is GateContract.Event.OnWriteBle -> reduce(viewState.value, event)
is GateContract.Event.OnPowerChanged -> reduce(viewState.value, event) is GateContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is GateContract.Event.OnTxSelect -> reduce(viewState.value, event) is GateContract.Event.OnTxSelect -> reduce(viewState.value, event)
@ -102,6 +43,7 @@ class GateViewModel @Inject constructor(
is GateContract.Event.OnHistoryIntervalSelect -> reduce(viewState.value, event) is GateContract.Event.OnHistoryIntervalSelect -> reduce(viewState.value, event)
is GateContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event) is GateContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event)
is GateContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event) is GateContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event)
is GateContract.Event.OnReload -> reduce(viewState.value, event)
} }
} }
@ -112,7 +54,17 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
state.gate.hostState.readInterval = event.interval setState {
state.copy(
gate = state.gate.copy(
gateState = state.gate.gateState.copy(
readInterval = event.interval
)
)
)
}
} }
@ -126,7 +78,7 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
setEffect { setEffect {
GateContract.Effect.Navigation.ReadIntervalSelector(state.gate.hostState.readInterval.toInt()) GateContract.Effect.Navigation.ReadIntervalSelector(state.gate.gateState.readInterval.toInt())
} }
} }
@ -140,7 +92,16 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
state.gate.hostState.historyInterval = event.interval setState {
state.copy(
gate = state.gate.copy(
gateState = state.gate.gateState.copy(
historyInterval = event.interval
)
)
)
}
} }
@ -154,7 +115,7 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
setEffect { setEffect {
GateContract.Effect.Navigation.HistoryIntervalSelector(state.gate.hostState.historyInterval.toInt()) GateContract.Effect.Navigation.HistoryIntervalSelector(state.gate.gateState.historyInterval.toInt())
} }
} }
@ -198,7 +159,15 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
state.gate.state.tx = event.tx setState {
state.copy(
gate = state.gate.copy(
state = state.gate.state.copy(
tx = event.tx
)
)
)
}
} }
@ -212,7 +181,11 @@ class GateViewModel @Inject constructor(
if(state is GateContract.State.Display) { if(state is GateContract.State.Display) {
setEffect { GateContract.Effect.Navigation.TxSelector(state.gate.state.tx) } setEffect {
GateContract.Effect.Navigation.TxSelector(
state.gate.state.tx
)
}
} }
@ -243,21 +216,12 @@ class GateViewModel @Inject constructor(
private fun reduce( private fun reduce(
state: GateContract.State, state: GateContract.State,
event: GateContract.Event.OnHideWriteBlePreview event: GateContract.Event.OnWriteBle
) {
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowWriteBlePreview
) { ) {
if(state is GateContract.State.Display){ if(state is GateContract.State.Display){
val newBle = bleViewMapper.map(state.gate) as Ble.Gate val newBle = state.gate
val writeRequest = Ble.Gate.WriteRequest( val writeRequest = Ble.Gate.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx, tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
@ -274,7 +238,7 @@ class GateViewModel @Inject constructor(
} }
setEffect { setEffect {
GateContract.Effect.ShowWriteBlePreview GateContract.Effect.Navigation.GateWrite(state.gate.info.serial, writeRequest)
} }
} }
@ -283,63 +247,61 @@ class GateViewModel @Inject constructor(
private fun reduce( private fun reduce(
state: GateContract.State, state: GateContract.State,
event: GateContract.Event.OnWriteBle event: GateContract.Event.OnReload
) { ) {
if(state is GateContract.State.Display){ loadData()
state.writeState?.let { request -> }
if(request is GateContract.State.Display.WriteState.DisplayPreview) { private var loadJob: Job? = null
viewModelScope.launch { private fun loadData(){
val params = GateScreenDestination.argsFrom(savedStateHandle)
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState { setState {
state.copy( GateContract.State.Loading(null)
writeState = GateContract.State.Display.WriteState.Writing(request.writeRequest)
)
} }
val currentState = viewState.value val ble = retryUntilNotNull(
onNewAttempt = {
if(currentState is GateContract.State.Display) {
val newBleObject = Ble.Gate(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
gateState = currentState.origin.gateState.copy(
historyInterval = request.writeRequest.interval
?: currentState.origin.gateState.historyInterval
)
)
writeBle(state.gate.info.serial, request.writeRequest).fold(
onSuccess = {
setState { setState {
currentState.copy( GateContract.State.Loading(it)
origin = newBleObject,
gate = bleMapper.map(newBleObject) as BleView.Gate,
writeState = GateContract.State.Display.WriteState.Success
)
} }
}, }
onFailure = { ){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if (ble is Ble.Gate) {
setState { setState {
state.copy(
writeState = GateContract.State.Display.WriteState.Failure when (this) {
is GateContract.State.Display -> {
copy(
origin = Ble.Gate(
info = ble.info,
state = origin.state,
gateState = origin.gateState
)
) )
} }
}
is GateContract.State.Loading -> {
GateContract.State.Display(
origin = ble,
gate = ble,
writeState = null
) )
}
} }
} }
}
} }
} }

View File

@ -1,6 +1,8 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main.view package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -16,16 +18,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.domain.model.Ble import llc.arma.ble.app.ui.screen.locale.value
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
@ -37,9 +35,12 @@ fun DisplayState(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.weight(1f)
.verticalScroll(scrollState) .verticalScroll(scrollState)
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
@ -70,10 +71,11 @@ fun DisplayState(
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Интервал измерений", title = "Интервал измерений",
subtitle = state.gate.hostState.historyInterval subtitle = state.gate.gateState.historyInterval
.toDuration(DurationUnit.MILLISECONDS) .toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ -> .toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." }, "$hours ч. $minutes мин. $seconds сек."
},
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.KeyboardArrowDown, imageVector = Icons.Rounded.KeyboardArrowDown,
@ -87,10 +89,11 @@ fun DisplayState(
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Интервал чтения", title = "Интервал чтения",
subtitle = state.gate.hostState.readInterval subtitle = state.gate.gateState.readInterval
.toDuration(DurationUnit.MILLISECONDS) .toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ -> .toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." }, "$hours ч. $minutes мин. $seconds сек."
},
icon = { icon = {
Icon( Icon(
imageVector = Icons.Rounded.KeyboardArrowDown, imageVector = Icons.Rounded.KeyboardArrowDown,
@ -142,15 +145,32 @@ fun DisplayState(
} }
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.gate) {
Button( Button(
onClick = { onClick = {
viewModel.setEvent(GateContract.Event.OnShowWriteBlePreview) viewModel.setEvent(GateContract.Event.OnWriteBle)
}, },
modifier = Modifier modifier = Modifier
.padding(16.dp)
.fillMaxWidth() .fillMaxWidth()
.height(48.dp) .height(48.dp)
) { ) {
Text(text = "Сохранить")
Text(
text = "Сохранить"
)
}
}
} }
} }

View File

@ -1,326 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.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.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
fun Write(
state: GateContract.State.Display.WriteState,
onEvent: (GateContract.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 GateContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.interval != null || state.writeRequest.readInterval !== 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 / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин. $seconds сек."
)
}
}
}
}
state.writeRequest.readInterval?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал чтения"
)
val hours = it / millisInHour
val minutes = (it - (hours * millisInHour)) / millisInMinute
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин. $seconds сек."
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Записать"
) {
onEvent(GateContract.Event.OnWriteBle)
}
SecondaryButton(
label = "Отменить"
) {
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
is GateContract.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))
SecondaryButton(
label = "Отменить"
) {
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
}
GateContract.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))
PrimaryButton(
label = "Ок"
) {
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
}
GateContract.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))
PrimaryButton(
label = "Ок"
) {
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
}
}
}
}

View File

@ -10,14 +10,16 @@ class GateBleTableContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnHideWritePreview: Event() data object OnWriteTable: Event()
data object OnWritePreview: Event()
data object OnWrite: Event()
data object OnRestart : Event() data object OnRestart : Event()
data object OnSelectBle : Event()
data class OnBleSelected(
val bleSerials: List<BleName>
) : Event()
data class OnAddBle( data class OnAddBle(
val ble: BleName val ble: BleName
) : Event() ) : Event()
@ -26,34 +28,14 @@ class GateBleTableContract {
sealed class State : ViewState { sealed class State : ViewState {
data object Loading : State() data class Loading(
val attempt: Int?
data object Error : State() ) : State()
data class Display( data class Display(
val bleAround: List<BleInfo>,
val newTable: List<BleName>, val newTable: List<BleName>,
val savedBleTable: List<BleName>, val savedBleTable: List<BleName>,
val writeState: WriteState? ) : State()
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: List<BleName>
) : WriteState()
data class Writing(
val writeRequest: List<BleName>
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
} }
@ -61,6 +43,14 @@ class GateBleTableContract {
sealed class Navigation : Effect() { sealed class Navigation : Effect() {
data class WriteTable(
val table: List<BleName>
) : Navigation()
data class BleSelector(
val selected: List<BleName>
) : Navigation()
data object Up : Navigation() data object Up : Navigation()
} }

View File

@ -1,24 +1,24 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.activity.compose.BackHandler import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.RemoveCircleOutline import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -33,34 +33,33 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.BleSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BleTableWriteScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.flow.launchIn import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.flow.onEach import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.rememberBottomDialogState import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.ble.BleItem
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName import llc.arma.ble.domain.model.BleName
@Destination<RootGraph> @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun GateBleTableScreen( fun GateBleTableScreen(
bleSerial: String, bleSerial: String,
navigator: DestinationsNavigator navigator: DestinationsNavigator,
resultRecipient: ResultRecipient<BleSelectorScreenDestination, Array<BleName>>,
writeResult: ResultRecipient<BleTableWriteScreenDestination, Boolean>
) { ) {
val viewModel = hiltViewModel<GateBleTableViewModel>() val viewModel = hiltViewModel<GateBleTableViewModel>()
@ -71,41 +70,31 @@ fun GateBleTableScreen(
when(it){ when(it){
GateBleTableContract.Effect.Navigation.Up -> GateBleTableContract.Effect.Navigation.Up ->
navigator.navigateUp() navigator.navigateUp()
is GateBleTableContract.Effect.Navigation.BleSelector ->
navigator.navigate(BleSelectorScreenDestination(it.selected.toTypedArray()))
is GateBleTableContract.Effect.Navigation.WriteTable ->
navigator.navigate(BleTableWriteScreenDestination(bleSerial, it.table.toTypedArray()))
} }
} }
} }
var showSelector by remember { writeResult.onResult {
mutableStateOf(false) if(it) viewModel.setEvent(GateBleTableContract.Event.OnRestart)
} }
BackHandler(showSelector) { resultRecipient.onResult {
showSelector = false viewModel.setEvent(GateBleTableContract.Event.OnBleSelected(it.toList()))
} }
val scope = rememberCoroutineScope() Scaffold(
val bottomDialog = rememberBottomDialogState() topBar = {
BackHandler(bottomDialog.sheetState?.isVisible == true) {
scope.launch {
bottomDialog.hide()
}
}
Column(
modifier = Modifier.fillMaxSize()
) {
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
IconButton(onClick = { IconButton(
if(showSelector){ onClick = navigator::popBackStack
showSelector = false ) {
} else {
navigator.popBackStack()
}
}) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack, imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null contentDescription = null
@ -114,102 +103,72 @@ fun GateBleTableScreen(
}, },
title = { title = {
Text( Text(
modifier = Modifier.weight(1f), text = "Таблица BLE",
text = if(showSelector){
"Выберите BLE"
} else {
"Таблица BLE ID"
},
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
}, },
actions = { actions = {
if(showSelector.not()){
IconButton( IconButton(
enabled = state is GateBleTableContract.State.Display, enabled = state is GateBleTableContract.State.Display,
onClick = { showSelector=true } onClick = {
viewModel.setEvent(GateBleTableContract.Event.OnSelectBle)
}
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.Add, imageVector = Icons.Rounded.Add,
contentDescription = null contentDescription = null
) )
} }
}
}
}
) )
if(state is GateBleTableContract.State.Loading){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
ContainedLoadingIndicator()
} }
}
if(state is GateBleTableContract.State.Error){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(it)
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.widthIn(max = 200.dp)
) { ) {
Text( when(state){
textAlign = TextAlign.Center, is GateBleTableContract.State.Display -> DisplayState(viewModel, state)
text = "Во время загрузки произошла ошибка", is GateBleTableContract.State.Loading -> LoadingState(navigator, viewModel, state)
) }
PrimaryButton( }
label = "Повторить" }
}
@Composable
private fun LoadingState(
navigator: DestinationsNavigator,
viewModel: GateBleTableViewModel,
state: GateBleTableContract.State.Loading
){ ){
viewModel.setEvent(GateBleTableContract.Event.OnRestart) Box(
} contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
}
}
}
if (state is GateBleTableContract.State.Display) {
if(showSelector) {
BleSelectorScreen(
saved = state.savedBleTable.map { it.serial },
selected = state.newTable.map { it.serial },
bleList = state.bleAround,
onClose = {
showSelector = false
}
){ ){
viewModel.setEvent(
GateBleTableContract.Event.OnAddBle( RetryingLoadingTemplate(state.attempt) {
BleName( navigator.navigateUp()
serial = it.serial,
name = it.name
)
)
)
} }
} else { }
}
@Composable
private fun DisplayState(
viewModel: GateBleTableViewModel,
state: GateBleTableContract.State.Display
){
var editBle by remember { var editBle by remember {
mutableStateOf<BleName?>(null) mutableStateOf<BleName?>(null)
} }
Column {
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier modifier = Modifier
@ -218,7 +177,8 @@ fun GateBleTableScreen(
) { ) {
val savedBleSerials = state.savedBleTable.map { it.serial } val savedBleSerials = state.savedBleTable.map { it.serial }
val newBle = state.newTable.filterNot { ble -> savedBleSerials.contains(ble.serial) } val newBle =
state.newTable.filterNot { ble -> savedBleSerials.contains(ble.serial) }
if (newBle.isNotEmpty()) { if (newBle.isNotEmpty()) {
@ -241,19 +201,7 @@ fun GateBleTableScreen(
editBle = it editBle = it
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it)) viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
}, },
shapeType = if(newBle.size == 1){ shapeType = newBle.takeShapeType(it)
ShapeType.Singleton
} else {
if(newBle.indexOf(it) == 0){
ShapeType.Start
} else {
if(newBle.indexOf(it) == newBle.size - 1){
ShapeType.End
} else {
ShapeType.Middle
}
}
}
) { ) {
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it)) viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
} }
@ -298,10 +246,30 @@ fun GateBleTableScreen(
} }
PrimaryButton( Box(
label = "Записать" modifier = Modifier.fillMaxWidth().animateContentSize()
) { ) {
viewModel.setEvent(GateBleTableContract.Event.OnWritePreview)
if (state.savedBleTable.sortedBy { it.serial } != state.newTable.sortedBy { it.serial }) {
Button(
onClick = {
viewModel.setEvent(GateBleTableContract.Event.OnWriteTable)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
}
} }
if (editBle != null) { if (editBle != null) {
@ -358,98 +326,6 @@ fun GateBleTableScreen(
} }
LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) {
if (bottomDialog.sheetState?.isVisible?.not() == true) {
viewModel.setEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
LaunchedEffect(key1 = state.writeState) {
if (state.writeState == null) {
bottomDialog.hide()
} else {
bottomDialog.show {
Write(
state = state.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
}
}
}
}
}
@Composable
private fun DisplayState(){
}
@Composable
fun BleSelectorScreen(
saved: List<String>,
selected: List<String>,
bleList: List<BleInfo>,
onClose: () -> Unit,
onAddBle: (BleInfo) -> Unit
) {
Column(
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
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 == ble.serial },
onCheckedChange = null
)
BleItem(
shapeType = bleList.filterNot { saved.contains(it.serial) }.takeShapeType(ble),
ble = ble
) {
onAddBle(ble)
}
}
}
}
PrimaryButton(
label = "Сохранить",
onClick = onClose
)
}
} }
@Composable @Composable

View File

@ -2,13 +2,14 @@ package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.domain.model.BleName import llc.arma.ble.domain.model.BleName
import llc.arma.ble.domain.usecase.AddBleToHostTable import llc.arma.ble.domain.usecase.AddBleToHostTable
import llc.arma.ble.domain.usecase.GetBleNamesFlow import llc.arma.ble.domain.usecase.GetBleNamesFlow
@ -18,10 +19,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class GateBleTableViewModel @Inject constructor( class GateBleTableViewModel @Inject constructor(
private val getFoundBle: GetFoundBle,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow, private val getBleNamesFlow: GetBleNamesFlow,
private val addBleToHostTable: AddBleToHostTable,
private val getHostBleTableBySerial: GetHostBleTableBySerial private val getHostBleTableBySerial: GetHostBleTableBySerial
) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() { ) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() {
@ -29,110 +28,66 @@ class GateBleTableViewModel @Inject constructor(
setEvent(GateBleTableContract.Event.OnRestart) setEvent(GateBleTableContract.Event.OnRestart)
viewModelScope.launch {
while (true){
val state = viewState.value
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(bleAround = getFoundBle())
} }
} override fun setInitialState() = GateBleTableContract.State.Loading(null)
delay(1_000)
}
}
}
override fun setInitialState() = GateBleTableContract.State.Loading
override fun handleEvents(event: GateBleTableContract.Event) { override fun handleEvents(event: GateBleTableContract.Event) {
when(event){ when(event){
is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event) is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event) is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWritePreview -> reduce(viewState.value, event) is GateBleTableContract.Event.OnWriteTable -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnHideWritePreview -> reduce(viewState.value, event) is GateBleTableContract.Event.OnSelectBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWrite -> reduce(viewState.value, event) is GateBleTableContract.Event.OnBleSelected -> reduce(viewState.value, event)
} }
} }
private fun reduce( private fun reduce(
state: GateBleTableContract.State, state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWrite event: GateBleTableContract.Event.OnSelectBle
) {
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
if(state is GateBleTableContract.State.Display) {
viewModelScope.launch {
setState {
state.copy(
writeState = GateBleTableContract.State.Display.WriteState.Writing(state.newTable)
)
}
addBleToHostTable.invoke(
serial = params.bleSerial,
ble = state.newTable
).fold(
onSuccess = {
setState {
state.copy(
writeState = GateBleTableContract.State.Display.WriteState.Success
)
}
setEvent(GateBleTableContract.Event.OnRestart)
},
onFailure = {
setState {
state.copy(
writeState = GateBleTableContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnHideWritePreview
) { ) {
if(state is GateBleTableContract.State.Display) { if(state is GateBleTableContract.State.Display) {
setState { setEffect {
state.copy(writeState = null) GateBleTableContract.Effect.Navigation.BleSelector(
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWritePreview
) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(writeState = GateBleTableContract.State.Display.WriteState.DisplayPreview(
state.newTable state.newTable
) )
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnBleSelected
) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(
newTable = event.bleSerials
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWriteTable
) {
if(state is GateBleTableContract.State.Display) {
setEffect {
GateBleTableContract.Effect.Navigation.WriteTable(
state.newTable
) )
} }
@ -165,6 +120,8 @@ class GateBleTableViewModel @Inject constructor(
} }
private var loadJob: Job? = null
private fun reduce( private fun reduce(
state: GateBleTableContract.State, state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnRestart event: GateBleTableContract.Event.OnRestart
@ -174,37 +131,35 @@ class GateBleTableViewModel @Inject constructor(
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle) val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState { setState {
GateBleTableContract.State.Loading GateBleTableContract.State.Loading(null)
} }
viewModelScope.launch { loadJob?.cancel()
loadJob = viewModelScope.launch {
val names = getBleNamesFlow.invoke().first() val names = getBleNamesFlow.invoke().first()
getHostBleTableBySerial(params.bleSerial).fold( val table = retryUntilNotNull(
onSuccess = { onNewAttempt = {
setState {
GateBleTableContract.State.Loading(it)
}
}
){
getHostBleTableBySerial(params.bleSerial).getOrNull()
}
val savedBle = it.map { ble -> BleName( val savedBle = table.map { ble -> BleName(
name = names.firstOrNull { it.serial == ble }?.name ?: "Безымянный", name = names.firstOrNull { it.serial == ble }?.name ?: "Безымянный",
serial = ble) } serial = ble) }
setState { setState {
GateBleTableContract.State.Display( GateBleTableContract.State.Display(
bleAround = emptyList(),
newTable = savedBle, newTable = savedBle,
savedBleTable = savedBle, savedBleTable = savedBle
writeState = null
) )
} }
},
onFailure = {
setState {
GateBleTableContract.State.Error
}
}
)
} }
} }

View File

@ -1,205 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.ShapeType
@Composable
fun Write(
state: GateBleTableContract.State.Display.WriteState,
onEvent: (GateBleTableContract.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 GateBleTableContract.State.Display.WriteState.DisplayPreview -> {
Box(
modifier = Modifier
.weight(1f)
.padding(vertical = 0.dp, horizontal = 8.dp)
) {
LazyColumn(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
item {
Text(
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
text = "Новая таблица BLE",
)
}
items(items = state.writeRequest) {
SelectBleItem(ShapeType.Singleton, it)
}
if(state.writeRequest.isEmpty()){
item {
Text(
textAlign = TextAlign.Center,
text = "Пусто",
modifier = Modifier.padding(48.dp).fillMaxWidth()
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Записать"
) {
onEvent(GateBleTableContract.Event.OnWrite)
}
SecondaryButton (
label = "Отменить"
) {
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
is GateBleTableContract.State.Display.WriteState.Writing -> {
Column {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
SecondaryButton (
label = "Отменить"
) {
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
GateBleTableContract.State.Display.WriteState.Success -> {
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))
PrimaryButton(
label = "Ok"
) {
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
GateBleTableContract.State.Display.WriteState.Failure -> {
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))
PrimaryButton(
label = "Ok"
) {
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package llc.arma.ble.app.ui.screen.inspection.selector.ble
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
class BleSelectorContract {
sealed class Event : ViewEvent {
data class OnSelect(
val ble: BleName
) : Event()
}
data class State(
val selected: List<BleName>,
val ble: List<BleInfo>
) : ViewState
sealed class Effect : ViewSideEffect
}

View File

@ -0,0 +1,152 @@
package llc.arma.ble.app.ui.screen.inspection.selector.ble
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.result.ResultBackNavigator
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.ble.BleItem
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun BleSelectorScreen(
selected: Array<BleName>,
resultNavigator: ResultBackNavigator<Array<BleName>>
) {
val viewModel = hiltViewModel<BleSelectorViewModel>()
val state = viewModel.viewState.value
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
resultNavigator.navigateBack()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
text = BleInfo.Type.HOST.localized
)
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
DisplayState(viewModel, state, resultNavigator)
}
}
}
@Composable
private fun DisplayState(
viewModel: BleSelectorViewModel,
state: BleSelectorContract.State,
resultNavigator: ResultBackNavigator<Array<BleName>>
){
Column(
modifier = Modifier
) {
if(state.ble.isEmpty()){
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f)
) {
items(
items = state.ble
) { ble ->
val checked = state.selected.map { it.serial }.contains(ble.serial)
BleItem(
checked = checked,
shapeType = state.ble.takeShapeType(ble),
ble = ble
) {
viewModel.setEvent(BleSelectorContract.Event.OnSelect(
BleName(ble.serial, ble.name)
))
}
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
Button(
onClick = {
resultNavigator.navigateBack(state.selected.toTypedArray())
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Выбрать"
)
}
}
}
}

View File

@ -0,0 +1,64 @@
package llc.arma.ble.app.ui.screen.inspection.selector.ble
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.BleSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.GetBleAroundFlow
import javax.inject.Inject
@HiltViewModel
class BleSelectorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel<BleSelectorContract.State, BleSelectorContract.Event, BleSelectorContract.Effect>() {
init {
val params = BleSelectorScreenDestination.argsFrom(savedStateHandle)
setState {
BleSelectorContract.State(
params.selected.toList(),
emptyList()
)
}
getBleAroundFlow.invoke().onEach {
setState {
copy(ble = it)
}
}.launchIn(viewModelScope)
}
override fun setInitialState() = BleSelectorContract.State(emptyList(), emptyList())
override fun handleEvents(event: BleSelectorContract.Event) {
when(event){
is BleSelectorContract.Event.OnSelect -> reduce(viewState.value, event)
}
}
private fun reduce(
state: BleSelectorContract.State,
event: BleSelectorContract.Event.OnSelect,
) {
val selected = if(state.selected.map { it.serial }.contains(event.ble.serial)){
val copy = state.selected.toMutableList()
copy.removeIf { it.serial == event.ble.serial }
copy
} else {
state.selected.toMutableList() + event.ble
}
setState {
copy(selected = selected.toMutableList())
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -14,24 +15,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -40,11 +34,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
@Serializable @Serializable
data class DurationSelectResult( data class DurationSelectResult(
@ -58,7 +48,7 @@ fun DurationSelectorScreen(
qualifier: String? = null, qualifier: String? = null,
duration: Int?, duration: Int?,
minimum: Int = 10_000, minimum: Int = 10_000,
maximum: Int = 10 * 24 * 60 * 60 * 1000, maximum: Int = 864_000_000,
daysComponent: Boolean = true, daysComponent: Boolean = true,
hoursComponent: Boolean = true, hoursComponent: Boolean = true,
minutesComponent: Boolean = true, minutesComponent: Boolean = true,
@ -82,17 +72,16 @@ fun DurationSelectorScreen(
} }
Column( Column(
modifier = Modifier.padding(16.dp).fillMaxWidth() verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier.padding(20.dp).fillMaxWidth()
) { ) {
Text( Text(
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier,
text = "Интервал измерений", text = "Интервал измерений",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
Spacer(modifier = Modifier.height(16.dp))
DurationPicker( DurationPicker(
minInterval = minimum, minInterval = minimum,
maxInterval = maximum, maxInterval = maximum,
@ -106,13 +95,25 @@ fun DurationSelectorScreen(
viewModel.setEvent(DurationSelectorContract.Event.OnDurationChanged(it)) viewModel.setEvent(DurationSelectorContract.Event.OnDurationChanged(it))
} }
Spacer(modifier = Modifier.height(16.dp)) Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
resultNavigator.navigateBack()
},
) {
Text(
text = "Отмена"
)
}
Button( Button(
onClick = { onClick = {
viewModel.setEvent(DurationSelectorContract.Event.OnSave) viewModel.setEvent(DurationSelectorContract.Event.OnSave)
}, }
modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = "Применить" text = "Применить"
@ -124,10 +125,12 @@ fun DurationSelectorScreen(
} }
}
@Composable @Composable
fun DurationPicker( fun DurationPicker(
modifier: Modifier, modifier: Modifier,
maxInterval: Int = 10 * 24 * 60 * 60 * 1000, maxInterval: Int = 864_000_000,
minInterval: Int = 10_000, minInterval: Int = 10_000,
seconds: Boolean = true, seconds: Boolean = true,
minutes: Boolean = true, minutes: Boolean = true,
@ -137,6 +140,7 @@ fun DurationPicker(
onChanged: (duration: Int) -> Unit onChanged: (duration: Int) -> Unit
){ ){
LaunchedEffect(value, maxInterval, minInterval) {
if(value > maxInterval){ if(value > maxInterval){
onChanged(maxInterval) onChanged(maxInterval)
} }
@ -144,6 +148,7 @@ fun DurationPicker(
if(value < minInterval){ if(value < minInterval){
onChanged(minInterval) onChanged(minInterval)
} }
}
val maxSeconds = maxInterval / millisInSecond val maxSeconds = maxInterval / millisInSecond
val maxMinutes = maxInterval / millisInMinute val maxMinutes = maxInterval / millisInMinute
@ -155,13 +160,20 @@ fun DurationPicker(
val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute
val secondsValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour) - (minutesValue * millisInMinute)) / millisInSecond val secondsValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour) - (minutesValue * millisInMinute)) / millisInSecond
println("${maxInterval} ${minInterval} ${maxDays} ${maxHours} ${maxMinutes} ${maxSeconds}")
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
) { ) {
if(days) { if(days) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker( NumberPicker(
range = -1..maxDays, range = -1..maxDays,
value = dayValue, value = dayValue,
@ -170,15 +182,19 @@ fun DurationPicker(
} }
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = "Д.") Text(text = "Д.")
} }
}
if(hours) { if(hours) {
Spacer(modifier = Modifier.width(16.dp)) Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker( NumberPicker(
range = -1..maxHours, range = -1..maxHours,
@ -188,15 +204,19 @@ fun DurationPicker(
} }
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = "Ч.") Text(text = "Ч.")
} }
}
if(minutes) { if(minutes) {
Spacer(modifier = Modifier.width(16.dp)) Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker( NumberPicker(
range = -1..maxMinutes, range = -1..maxMinutes,
@ -206,15 +226,19 @@ fun DurationPicker(
} }
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = "М.") Text(text = "М.")
} }
}
if(seconds) { if(seconds) {
Spacer(modifier = Modifier.width(16.dp)) Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker( NumberPicker(
range = -1..maxSeconds, range = -1..maxSeconds,
@ -224,7 +248,7 @@ fun DurationPicker(
} }
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(4.dp))
Text(text = "С.") Text(text = "С.")
@ -234,6 +258,8 @@ fun DurationPicker(
} }
}
const val millisInSecond = 1000 const val millisInSecond = 1000
const val millisInMinute = millisInSecond * 60 const val millisInMinute = millisInSecond * 60
const val millisInHour = millisInMinute * 60 const val millisInHour = millisInMinute * 60
@ -250,6 +276,7 @@ fun NumberPicker(
LaunchedEffect(range){ LaunchedEffect(range){
if(value > range.last){ if(value > range.last){
onValueChanged(range.last) onValueChanged(range.last)
@ -264,12 +291,14 @@ fun NumberPicker(
} }
println(value)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
){ ){
FilledIconButton( FilledTonalIconButton(
onClick = { onClick = {
if(value < range.last) onValueChanged(value + 1) if(value < range.last) onValueChanged(value + 1)
} }
@ -304,7 +333,7 @@ fun NumberPicker(
Spacer(modifier = Modifier.height(36.dp)) Spacer(modifier = Modifier.height(36.dp))
FilledIconButton( FilledTonalIconButton(
onClick = { onClick = {
if(value > range.first) onValueChanged(value - 1) if(value > range.first) onValueChanged(value - 1)

View File

@ -1,7 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.selector.duration package llc.arma.ble.app.ui.screen.inspection.selector.duration
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel

View File

@ -3,7 +3,7 @@ package llc.arma.ble.app.ui.screen.inspection.selector.power
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.domain.model.Ble
class TxPowerSelectorContract { class TxPowerSelectorContract {
@ -12,7 +12,7 @@ class TxPowerSelectorContract {
data object OnNavigateUp : Event() data object OnNavigateUp : Event()
data class OnSelected( data class OnSelected(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Event() ) : Event()
data object OnSave : Event() data object OnSave : Event()
@ -20,7 +20,7 @@ class TxPowerSelectorContract {
} }
data class State( data class State(
val tx: BleView.BleState.TX? val tx: Ble.BleState.TX?
) : ViewState ) : ViewState
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
@ -30,7 +30,7 @@ class TxPowerSelectorContract {
data object NavigateUp : Navigation() data object NavigateUp : Navigation()
data class NavigateUpWithResult( data class NavigateUpWithResult(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Navigation() ) : Navigation()
} }

View File

@ -1,37 +1,41 @@
package llc.arma.ble.app.ui.screen.inspection.selector.power package llc.arma.ble.app.ui.screen.inspection.selector.power
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Button
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultBackNavigator
import llc.arma.ble.app.ui.common.PrimaryButton import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.model.BleView import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.SelectorItem import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.powerPercentage
import llc.arma.ble.app.ui.screen.locale.value
import llc.arma.ble.domain.model.Ble
@Destination<RootGraph>(style = DestinationStyleBottomSheet::class) @Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@Composable @Composable
fun TxPowerSelectorScreen( fun TxPowerSelectorScreen(
tx: BleView.BleState.TX?, tx: Ble.BleState.TX?,
resultNavigator: ResultBackNavigator<BleView.BleState.TX> resultNavigator: ResultBackNavigator<Ble.BleState.TX>
) { ) {
val viewModel = hiltViewModel<TxPowerSelectorViewModel>() val viewModel = hiltViewModel<TxPowerSelectorViewModel>()
@ -48,16 +52,30 @@ fun TxPowerSelectorScreen(
} }
} }
Column { Surface(
shape = RoundedCornerShape(20.dp),
modifier = Modifier.fillMaxWidth(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(20.dp)
) {
Text( Text(
text = "Мощность", text = "Мощность",
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
BleView.BleState.TX.entries.forEach { Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Ble.BleState.TX.entries.forEach {
SelectorItem( SelectorItem(
shapeType = Ble.BleState.TX.entries.takeShapeType(it),
label = "${it.value} dBb (${it.powerPercentage} %)", label = "${it.value} dBb (${it.powerPercentage} %)",
selected = it == state.tx selected = it == state.tx
) { ) {
@ -66,14 +84,69 @@ fun TxPowerSelectorScreen(
} }
Spacer(modifier = Modifier.height(16.dp)) }
PrimaryButton( Row(
label = "Применить" horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) { ) {
OutlinedButton(
onClick = {
viewModel.setEvent(TxPowerSelectorContract.Event.OnNavigateUp)
}
) {
Text(
text = "Отмена"
)
}
Button(
onClick = {
viewModel.setEvent(TxPowerSelectorContract.Event.OnSave) viewModel.setEvent(TxPowerSelectorContract.Event.OnSave)
} }
) {
Text(
text = "Применить"
)
}
}
} }
} }
}
@Composable
fun SelectorItem(
shapeType: ShapeType,
label: String,
selected: Boolean,
onClick: () -> Unit
){
Surface(
shape = shapeType.shape,
color = MaterialTheme.colorScheme.surfaceContainer
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 12.dp, vertical = 10.dp)
) {
RadioButton(
selected = selected,
onClick = null
)
Text(text = label)
}
}
}

View File

@ -1,7 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.selector.power package llc.arma.ble.app.ui.screen.inspection.selector.power
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel

View File

@ -18,8 +18,13 @@ class ThermometerHistoryContract {
sealed class State : ViewState { sealed class State : ViewState {
data class Loading(
val attempt: Int?,
val progress: Float?
) : State()
data class Display( data class Display(
val loadingHistoryState : ProgressState<List<Ble.Thermometer.HistoryPoint>> val history : List<Ble.Thermometer.HistoryPoint>
) : State() ) : State()
data object Exception : State() data object Exception : State()

View File

@ -23,7 +23,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -33,14 +36,17 @@ import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntry import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -96,13 +102,9 @@ fun ThermometerHistoryScreen(
title = { title = {
val title = when(state){ val title = when(state){
is ThermometerHistoryContract.State.Display -> { is ThermometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) { "История (${state.history.size})"
is ProgressState.Finished -> "История (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "История"
is ProgressState.Progress -> "История"
} }
} else -> "История"
ThermometerHistoryContract.State.Exception -> "История"
} }
Text( Text(
@ -116,10 +118,7 @@ fun ThermometerHistoryScreen(
onClick = { onClick = {
viewModel.setEvent(ThermometerHistoryContract.Event.OnRefresh) viewModel.setEvent(ThermometerHistoryContract.Event.OnRefresh)
}, },
enabled = when(state){ enabled = state is ThermometerHistoryContract.State.Display
is ThermometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
ThermometerHistoryContract.State.Exception -> true
}
) { ) {
Icon( Icon(
imageVector = Icons.Rounded.Refresh, imageVector = Icons.Rounded.Refresh,
@ -138,6 +137,7 @@ fun ThermometerHistoryScreen(
when (state) { when (state) {
is ThermometerHistoryContract.State.Display -> DisplayState(state = state) is ThermometerHistoryContract.State.Display -> DisplayState(state = state)
ThermometerHistoryContract.State.Exception -> ExceptionState() ThermometerHistoryContract.State.Exception -> ExceptionState()
is ThermometerHistoryContract.State.Loading -> LoadingState(viewModel, state)
} }
} }
@ -146,23 +146,53 @@ fun ThermometerHistoryScreen(
} }
@Composable
private fun LoadingState(
viewModel: ThermometerHistoryViewModel,
state: ThermometerHistoryContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
RetryingLoadingTemplate(
state.attempt
) {
viewModel.setEvent(ThermometerHistoryContract.Event.OnNavigateUp)
}
}
/*val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
LoadingIndicator(
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)*/
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun DisplayState( private fun DisplayState(
state: ThermometerHistoryContract.State.Display state: ThermometerHistoryContract.State.Display
) { ) {
Box(modifier = Modifier Box(
modifier = Modifier
.padding(8.dp) .padding(8.dp)
.fillMaxSize() .fillMaxSize()
) { ) {
when (state.loadingHistoryState) { if(state.history.isEmpty()){
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
@ -171,8 +201,8 @@ private fun DisplayState(
} else { } else {
val producer = remember(state.loadingHistoryState.data) { val producer = remember(state.history) {
state.loadingHistoryState.data.mapIndexed { index, measurePoint -> state.history.mapIndexed { index, measurePoint ->
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}.let { }.let {
ChartEntryModelProducer(it) ChartEntryModelProducer(it)
@ -194,9 +224,11 @@ private fun DisplayState(
Chart( Chart(
chartScrollState = scrollState, chartScrollState = scrollState,
chartScrollSpec = rememberChartScrollSpec(initialScroll = InitialScroll.End),
chart = lineChart, chart = lineChart,
chartModelProducer = producer, chartModelProducer = producer,
startAxis = startAxis(), startAxis = startAxis(),
runInitialAnimation = false,
bottomAxis = bottomAxis( bottomAxis = bottomAxis(
tickLength = 0.dp, tickLength = 0.dp,
valueFormatter = axisValueFormatter, valueFormatter = axisValueFormatter,
@ -205,38 +237,6 @@ private fun DisplayState(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
LaunchedEffect(scrollState.maxValue) {
scrollState.scrollBy(scrollState.maxValue)
}
}
}
is ProgressState.Indeterminate -> {
ContainedLoadingIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
LoadingIndicator(
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
}
} }
} }

View File

@ -2,13 +2,13 @@ package llc.arma.ble.app.ui.screen.inspection.thermometer.history
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
import javax.inject.Inject import javax.inject.Inject
@ -24,8 +24,8 @@ class ThermometerHistoryViewModel @Inject constructor(
} }
override fun setInitialState() = ThermometerHistoryContract.State.Display( override fun setInitialState() = ThermometerHistoryContract.State.Loading(
ProgressState.Indeterminate null, null
) )
override fun handleEvents(event: ThermometerHistoryContract.Event) { override fun handleEvents(event: ThermometerHistoryContract.Event) {
@ -60,22 +60,21 @@ class ThermometerHistoryViewModel @Inject constructor(
val params = ThermometerHistoryScreenDestination.argsFrom(savedStateHandle) val params = ThermometerHistoryScreenDestination.argsFrom(savedStateHandle)
setState { setState {
ThermometerHistoryContract.State.Display(ProgressState.Indeterminate) ThermometerHistoryContract.State.Loading(null, null)
} }
getTemperatureHistoryBySerial(params.bleSerial).collect { val history = retryUntilNotNull(
it.fold( onNewAttempt = {
onSuccess = {
setState { setState {
ThermometerHistoryContract.State.Display(it) ThermometerHistoryContract.State.Loading(it, null)
} }
}, }
onFailure = { ){
getTemperatureHistoryBySerial(params.bleSerial).getOrNull()
}
setState { setState {
ThermometerHistoryContract.State.Exception ThermometerHistoryContract.State.Display(history)
}
}
)
} }
} }

View File

@ -1,6 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.main package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.foundation.background import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -21,13 +21,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.domain.model.Ble import llc.arma.ble.app.ui.screen.locale.value
@Composable @Composable
fun DisplayState( fun DisplayState(
@ -35,9 +32,12 @@ fun DisplayState(
state: ThermometerContract.State.Display state: ThermometerContract.State.Display
) { ) {
Column {
Column( Column(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
@ -69,29 +69,28 @@ fun DisplayState(
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Температура", title = "Температура",
subtitle = "${state.thermometer.thermometerState.temperature.value} °C", subtitle = "${state.thermometer.thermometerState.temperature} °C"
onClick = {
//onEvent(ThermometerContract.Event.OnPowerEdit)
}
) )
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "Сохранять измерения", title = "Сохранять измерения",
onClick = {
//onEvent(ThermometerContract.Event.OnPowerEdit)
},
icon = { icon = {
Switch( Switch(
checked = state.thermometer.thermometerState.saveHistory, checked = state.thermometer.thermometerState.saveHistory,
onCheckedChange = { onCheckedChange = {
viewModel.setEvent(ThermometerContract.Event.OnSaveHistoryChanged(it)) viewModel.setEvent(
ThermometerContract.Event.OnSaveHistoryChanged(it)
)
} }
) )
} }
) )
val hours = state.thermometer.thermometerState.historyInterval / 1000 / 60 / 60 if(state.thermometer.thermometerState.saveHistory) {
val hours =
state.thermometer.thermometerState.historyInterval / 1000 / 60 / 60
val minutes = val minutes =
(state.thermometer.thermometerState.historyInterval - (hours * 1000 * 60 * 60)) / 1000 / 60 (state.thermometer.thermometerState.historyInterval - (hours * 1000 * 60 * 60)) / 1000 / 60
@ -110,6 +109,8 @@ fun DisplayState(
} }
) )
}
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Middle, shapeType = ShapeType.Middle,
title = "График измерений", title = "График измерений",
@ -129,11 +130,20 @@ fun DisplayState(
} }
) )
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.thermometer) {
Button( Button(
onClick = { onClick = {
viewModel.setEvent(ThermometerContract.Event.OnShowWriteBlePreview) viewModel.setEvent(ThermometerContract.Event.OnShowWriteBlePreview)
}, },
modifier = Modifier modifier = Modifier
.padding(16.dp)
.fillMaxWidth() .fillMaxWidth()
.height(48.dp) .height(48.dp)
) { ) {
@ -146,6 +156,10 @@ fun DisplayState(
} }
}
}
@Composable @Composable
fun BleMenuItem( fun BleMenuItem(
shapeType: ShapeType, shapeType: ShapeType,

View File

@ -2,23 +2,29 @@ package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun LoadingState(){ fun LoadingState(
viewModel: ThermometerViewModel,
state: ThermometerContract.State.Loading,
){
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
){ ){
ContainedLoadingIndicator() RetryingLoadingTemplate(state.attempt){
viewModel.setEvent(ThermometerContract.Event.OnNavigateUp)
}
} }

View File

@ -3,19 +3,16 @@ package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import llc.arma.ble.app.ui.common.ViewEvent import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState 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.Ble
class ThermometerContract { class ThermometerContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnRestart : Event()
data object OnTxSelect : Event() data object OnTxSelect : Event()
data object OnWriteBle : Event()
data object OnHideWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event() data object OnShowWriteBlePreview : Event()
data object OnShowTemperatureHistory : Event() data object OnShowTemperatureHistory : Event()
@ -27,7 +24,7 @@ class ThermometerContract {
) : Event() ) : Event()
data class OnPowerChanged( data class OnPowerChanged(
val tx: BleView.BleState.TX val tx: Ble.BleState.TX
) : Event() ) : Event()
data object OnSaveIntervalEdit : Event() data object OnSaveIntervalEdit : Event()
@ -42,42 +39,26 @@ class ThermometerContract {
sealed class State : ViewState { sealed class State : ViewState {
data object Loading : State() data class Loading(
val attempt: Int?
) : State()
data class Display( data class Display(
val origin: Ble.Thermometer, val origin: Ble.Thermometer,
val thermometer: BleView.Thermometer, val thermometer: Ble.Thermometer
val writeState: WriteState? ) : State()
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Thermometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Thermometer.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
} }
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
sealed class Navigation : Effect() { sealed class Navigation : Effect() {
data class Write(
val bleSerial: String,
val writeRequest: Ble.Thermometer.WriteRequest
) : Navigation()
data object Up : Navigation() data object Up : Navigation()
data class DurationSelector( data class DurationSelector(
@ -85,7 +66,7 @@ class ThermometerContract {
) : Navigation() ) : Navigation()
data class TxSelector( data class TxSelector(
val tx: BleView.BleState.TX? val tx: Ble.BleState.TX?
) : Navigation() ) : Navigation()
data class ChangePassword( data class ChangePassword(

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -23,54 +24,32 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import kotlin.time.DurationUnit
import kotlin.time.toDuration
enum class SheetPage {
WRITE
}
val Boolean.localizedName: String
get() {
return if(this){
"Включено"
} else {
"Выключено"
}
}
@Destination<RootGraph> @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ThermometerScreen( fun ThermometerScreen(
bleSerial: String, bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>, txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
durationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>, durationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
writeResult: ResultRecipient<ThermometerWriteScreenDestination, Boolean>,
navigator: DestinationsNavigator navigator: DestinationsNavigator
) { ) {
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val viewModel = hiltViewModel<ThermometerViewModel>() val viewModel = hiltViewModel<ThermometerViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult { txSelectResult.onResult {
viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it)) viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
} }
@ -79,44 +58,13 @@ fun ThermometerScreen(
viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalChanged(it.duration.toLong())) viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
} }
LaunchedEffect(sheetPage){ writeResult.onResult {
when(sheetPage){ if(it) viewModel.setEvent(ThermometerContract.Event.OnRestart)
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is ThermometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
}
}
}
else -> {
bottomDialog.hide()
}
}
} }
LaunchedEffect(Unit){ LaunchedEffect(Unit){
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
is ThermometerContract.Effect.HideWriteBle -> {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowWriteBle -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is ThermometerContract.Effect.Navigation.ChangePassword -> is ThermometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial)) navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
@ -136,8 +84,12 @@ fun ThermometerScreen(
)) ))
ThermometerContract.Effect.Navigation.Up -> ThermometerContract.Effect.Navigation.Up ->
navigator.popBackStack() navigator.navigateUp()
is ThermometerContract.Effect.Navigation.Write ->
navigator.navigate(ThermometerWriteScreenDestination(
it.bleSerial, it.writeRequest
))
} }
}.launchIn(this) }.launchIn(this)
@ -161,6 +113,20 @@ fun ThermometerScreen(
}, },
title = { title = {
Text(text = BleInfo.Type.THERMOMETER.localized) Text(text = BleInfo.Type.THERMOMETER.localized)
},
actions = {
if(state is ThermometerContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnRestart)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
} }
) )
} }
@ -172,7 +138,7 @@ fun ThermometerScreen(
when(state){ when(state){
is ThermometerContract.State.Display -> DisplayState(viewModel, state) is ThermometerContract.State.Display -> DisplayState(viewModel, state)
is ThermometerContract.State.Loading -> LoadingState() is ThermometerContract.State.Loading -> LoadingState(viewModel, state)
} }
} }

View File

@ -2,69 +2,30 @@ package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ThermometerViewModel @Inject constructor( class ThermometerViewModel @Inject constructor(
private val getBleBySerial: GetBleBySerial, private val getBleBySerial: GetBleBySerial,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() { ) : BaseViewModel<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() {
init { init {
val params = ThermometerScreenDestination.argsFrom(savedStateHandle) loadData()
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
if(ble != null && ble is Ble.Thermometer){
setState {
when(this){
is ThermometerContract.State.Display -> {
copy(
origin = Ble.Thermometer(
info = ble.info,
state = origin.state,
thermometerState = origin.thermometerState
)
)
}
ThermometerContract.State.Loading -> {
ThermometerContract.State.Display(
origin = ble,
thermometer = bleMapper.map(ble) as BleView.Thermometer,
writeState = null
)
}
}
}
}
} }
} override fun setInitialState() = ThermometerContract.State.Loading(null)
override fun setInitialState() = ThermometerContract.State.Loading
override fun handleEvents(event: ThermometerContract.Event) { override fun handleEvents(event: ThermometerContract.Event) {
when(event){ when(event){
@ -75,13 +36,21 @@ class ThermometerViewModel @Inject constructor(
is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event) is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event) is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event) is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is ThermometerContract.Event.OnTxSelect -> reduce(viewState.value, event) is ThermometerContract.Event.OnTxSelect -> reduce(viewState.value, event)
is ThermometerContract.Event.OnRestart -> reduce(viewState.value, event)
} }
} }
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnRestart
) {
loadData()
}
private fun reduce( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnTxSelect event: ThermometerContract.Event.OnTxSelect
@ -102,32 +71,6 @@ class ThermometerViewModel @Inject constructor(
setEffect { ThermometerContract.Effect.Navigation.Up } setEffect { ThermometerContract.Effect.Navigation.Up }
} }
/*private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnBleChanged
) {
when(state){
is ThermometerContract.State.Display -> setState {
state.copy(
origin = Ble.Thermometer(
info = event.ble.info,
state = state.origin.state,
thermometerState = state.origin.thermometerState
)
)
}
is ThermometerContract.State.Loading -> setState {
ThermometerContract.State.Display(
origin = event.ble,
thermometer = bleMapper.map(event.ble) as BleView.Thermometer,
writeState = null
)
}
}
}*/
private fun reduce( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit event: ThermometerContract.Event.OnSaveIntervalEdit
@ -146,7 +89,17 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display) { if(state is ThermometerContract.State.Display) {
state.thermometer.thermometerState.historyInterval = event.interval setState {
state.copy(
thermometer = state.thermometer.copy(
thermometerState = state.thermometer.thermometerState.copy(
historyInterval = event.interval
)
)
)
}
} }
@ -159,7 +112,17 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display) { if(state is ThermometerContract.State.Display) {
state.thermometer.state.tx = event.tx setState {
state.copy(
thermometer = state.thermometer.copy(
state = state.thermometer.state.copy(
tx = event.tx
)
)
)
}
} }
@ -171,7 +134,17 @@ class ThermometerViewModel @Inject constructor(
) { ) {
if(state is ThermometerContract.State.Display) { if(state is ThermometerContract.State.Display) {
state.thermometer.thermometerState.saveHistory = event.saveHistory setState {
state.copy(
thermometer = state.thermometer.copy(
thermometerState = state.thermometer.thermometerState.copy(
saveHistory = event.saveHistory
)
)
)
}
} }
@ -201,7 +174,7 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display){ if(state is ThermometerContract.State.Display){
val newBle = bleViewMapper.map(state.thermometer) as Ble.Thermometer val newBle = state.thermometer
val writeRequest = Ble.Thermometer.WriteRequest( val writeRequest = Ble.Thermometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx, tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
@ -209,110 +182,13 @@ class ThermometerViewModel @Inject constructor(
historyInterval = if(newBle.thermometerState.historyInterval == state.origin.thermometerState.historyInterval) null else newBle.thermometerState.historyInterval, historyInterval = if(newBle.thermometerState.historyInterval == state.origin.thermometerState.historyInterval) null else newBle.thermometerState.historyInterval,
) )
setState { setEffect {
state.copy( ThermometerContract.Effect.Navigation.Write(
writeState = ThermometerContract.State.Display.WriteState.DisplayPreview( state.thermometer.info.serial,
writeRequest writeRequest
) )
)
} }
setEffect {
ThermometerContract.Effect.ShowWriteBle
}
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnWriteBle
) {
if(state is ThermometerContract.State.Display){
state.writeState?.let { request ->
if(request is ThermometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = ThermometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
}
writeBle(state.thermometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is ThermometerContract.State.Display) {
val newBleObject = Ble.Thermometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
thermometerState = currentState.origin.thermometerState.copy(
saveHistory = request.writeRequest.saveHistory
?: currentState.origin.thermometerState.saveHistory,
historyInterval = request.writeRequest.historyInterval
?: currentState.origin.thermometerState.historyInterval,
)
)
setState {
currentState.copy(
origin = newBleObject,
thermometer = bleMapper.map(newBleObject) as BleView.Thermometer,
writeState = ThermometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = ThermometerContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnHideWriteBlePreview
) {
if(state is ThermometerContract.State.Display){
setState {
state.copy(
writeState = null
)
}
}
setEffect {
ThermometerContract.Effect.HideWriteBle
} }
} }
@ -332,4 +208,56 @@ class ThermometerViewModel @Inject constructor(
} }
private var loadJob: Job? = null
private fun loadData(){
val params = ThermometerScreenDestination.argsFrom(savedStateHandle)
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState {
ThermometerContract.State.Loading(null)
}
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
ThermometerContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if(ble is Ble.Thermometer){
setState {
when(this){
is ThermometerContract.State.Display -> {
copy(
origin = Ble.Thermometer(
info = ble.info,
state = origin.state,
thermometerState = origin.thermometerState
)
)
}
is ThermometerContract.State.Loading -> {
ThermometerContract.State.Display(
origin = ble,
thermometer = ble
)
}
}
}
}
}
}
} }

View File

@ -1,234 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
fun Write(
state: ThermometerContract.State.Display.WriteState,
onEvent: (ThermometerContract.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 ThermometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) {
state.writeRequest.tx?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db"
)
}
state.writeRequest.saveHistory?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = it.localizedName
)
}
state.writeRequest.historyInterval?.let {
val hours = it / 1000 / 60 / 60
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин."
)
}
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Записать"
) {
onEvent(ThermometerContract.Event.OnWriteBle)
}
SecondaryButton(
label = "Отменить"
) {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
}
}
is ThermometerContract.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))
SecondaryButton(
label = "Отменить"
) {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
ThermometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
Spacer(modifier = Modifier.height(20.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
ThermometerContract.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))
PrimaryButton(
label = "Ок"
) {
onEvent(ThermometerContract.Event.OnHideWriteBlePreview)
}
}
}
}
}
}
}

View File

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

View File

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

View File

@ -23,6 +23,38 @@ import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
val Ble.BleState.TX.powerPercentage: Int
get() {
return when(this){
Ble.BleState.TX.MINUS_40 -> 1
Ble.BleState.TX.MINUS_20 -> 5
Ble.BleState.TX.MINUS_16 -> 7
Ble.BleState.TX.MINUS_12 -> 10
Ble.BleState.TX.MINUS_8 -> 16
Ble.BleState.TX.MINUS_4 -> 20
Ble.BleState.TX.ZERO -> 40
Ble.BleState.TX.PLUS_3 -> 80
Ble.BleState.TX.PLUS_4 -> 100
}
}
val Ble.BleState.TX.value: Int
get() {
return when(this){
Ble.BleState.TX.MINUS_40 -> -40
Ble.BleState.TX.MINUS_20 -> -20
Ble.BleState.TX.MINUS_16 -> -16
Ble.BleState.TX.MINUS_12 -> -12
Ble.BleState.TX.MINUS_8 -> -8
Ble.BleState.TX.MINUS_4 -> -4
Ble.BleState.TX.ZERO -> 0
Ble.BleState.TX.PLUS_3 -> 3
Ble.BleState.TX.PLUS_4 -> 4
}
}
val Ble.BleState.TX.localizedName: String val Ble.BleState.TX.localizedName: String
get() { get() {
return when(this){ return when(this){

View File

@ -13,8 +13,8 @@ import com.ramcosta.composedestinations.generated.NavGraphs
fun MainScreen() { fun MainScreen() {
val navController = rememberNavController() val navController = rememberNavController()
val bottomSheetNavigator = rememberBottomSheetNavigator() val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider.addNavigator(bottomSheetNavigator) navController.navigatorProvider.addNavigator(bottomSheetNavigator)
ModalBottomSheetLayout( ModalBottomSheetLayout(
@ -27,351 +27,4 @@ fun MainScreen() {
) )
} }
//DestinationsNavHost(navGraph = NavGraphs.root)
/*val controller = rememberNavController()
NavHost(
navController = controller,
startDestination = BleListRoute,
builder = {
dialog<PasswordFormRoute> {
ChangePasswordScreen(
onNavigationEvent = {
when(it){
ChangePasswordContract.Effect.Navigation.Up ->
controller.popBackStack()
}
}
)
}
dialog<DurationSelectorRoute> {
val params = it.savedStateHandle.toRoute<DurationSelectorRoute>()
println(params.requestId)
ModalBottomSheet(
onDismissRequest = controller::popBackStack
) {
DurationSelectorScreen(
onNavigationEvent = {
when(it){
DurationSelectorContract.Effect.Navigation.Up ->
controller.popBackStack()
is DurationSelectorContract.Effect.Navigation.UpWithResult -> {
controller.previousBackStackEntry?.savedStateHandle?.set(
params.requestId,
it.duration
)
controller.popBackStack()
}
}
}
)
}
}
composable<BeaconRoute> {
val txSelectResult = controller.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<BleView.BleState.TX?>("txSelectResult", null)
?.collectAsState()?.value
BeaconScreen(
txSelectResult = txSelectResult,
onNavigationEvent = {
when(it){
is BeaconContract.Effect.Navigation.PasswordForm ->
controller.navigate(PasswordFormRoute(it.bleSerial))
BeaconContract.Effect.Navigation.Up ->
controller.popBackStack()
is BeaconContract.Effect.Navigation.TxSelector ->
controller.navigate(TxPowerSelectorRoute(it.tx))
}
}
)
}
composable<AccelerometerRoute> {
val txSelectResult = controller.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<BleView.BleState.TX?>("txSelectResult", null)
?.collectAsState()?.value
AccelerometerScreen (
onNavigationEvent = {
when(it){
is AccelerometerContract.Effect.Navigation.AccelHistory -> TODO()
is AccelerometerContract.Effect.Navigation.AccelRealtime -> TODO()
is AccelerometerContract.Effect.Navigation.AccelSpectre -> TODO()
is AccelerometerContract.Effect.Navigation.ChangePassword ->
controller.navigate(PasswordFormRoute(it.serial))
is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
controller.navigate(TxPowerSelectorRoute(it.tx))
}
}
)
}
composable<GateHistoryRoute> {
GateHistoryScreen(
onNavigationEvent = {
when(it){
GateHistoryContract.Effect.Navigation.NavigateUp ->
controller.popBackStack()
}
}
)
}
composable<GateBleTableRoute> {
GateBleTableScreen(
onEvent = {
when(it){
GateBleTableContract.Effect.Navigation.NavigateUp ->
controller.popBackStack()
}
}
)
}
composable<GateRoute> {
val savedState = remember {
controller.currentBackStackEntry
?.savedStateHandle
}
val txSelectResult = savedState
?.getStateFlow<BleView.BleState.TX?>("txSelectResult", null)
?.collectAsState()?.value
val readDurationSelectResult = savedState
?.getStateFlow<Int?>("GateReadDuration", null)
?.collectAsState()?.value
GateScreen(
readDurationSelectResult = readDurationSelectResult,
txSelectResult = txSelectResult,
onNavigationEvent = {
when(it){
is GateContract.Effect.Navigation.BleTable ->
controller.navigate(GateBleTableRoute(it.serial))
is GateContract.Effect.Navigation.ChangePassword ->
controller.navigate(PasswordFormRoute(it.serial))
is GateContract.Effect.Navigation.GateHistory ->
controller.navigate(GateHistoryRoute(it.ble.serial))
is GateContract.Effect.Navigation.Up ->
controller.popBackStack()
is GateContract.Effect.Navigation.TxSelector ->
controller.navigate(TxPowerSelectorRoute(it.tx))
is GateContract.Effect.Navigation.ReadIntervalSelector ->
controller.navigate(DurationSelectorRoute(
"GateReadDuration",
it.interval
))
}
}
)
controller.currentBackStackEntry
?.savedStateHandle
?.remove<BleView.BleState.TX?>("txSelectResult")
controller.currentBackStackEntry
?.savedStateHandle
?.remove<Int?>("GateReadDuration")
}
composable<ThermometerRoute> {
val txSelectResult = controller.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<BleView.BleState.TX?>("txSelectResult", null)
?.collectAsState()?.value
ThermometerScreen(
txSelectResult = txSelectResult,
onNavigationEvent = {
when(it){
ThermometerContract.Effect.Navigation.ChangePassword -> TODO()
is ThermometerContract.Effect.Navigation.ThermometerHistory ->
controller.navigate(ThermometerHistoryRoute(it.bleSerial))
ThermometerContract.Effect.Navigation.Up ->
controller.popBackStack()
is ThermometerContract.Effect.Navigation.TxSelector ->
controller.navigate(TxPowerSelectorRoute(it.tx))
}
}
)
controller.currentBackStackEntry
?.savedStateHandle
?.remove<BleView.BleState.TX?>("txSelectResult")
}
composable<TxPowerSelectorRoute> {
TxPowerSelectorScreen(
onNavigationEvent = {
when(it){
is TxPowerSelectorContract.Effect.Navigation.NavigateUp ->
controller.popBackStack()
is TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult -> {
controller.previousBackStackEntry?.savedStateHandle?.set(
"txSelectResult",
it.tx
)
controller.popBackStack()
}
}
}
)
}
composable<BleFilterRoute> {
BleFilterScreen {
when(it){
BleFilterContract.Effect.Navigation.NavigateUp ->
controller.popBackStack()
}
}
}
composable<ThermometerHistoryRoute> {
ThermometerHistoryScreen(
onNavigationEvent = {
when(it){
ThermometerHistoryContract.Effect.Navigation.NavigateUp ->
controller.popBackStack()
}
}
)
}
composable<BleListRoute> {
BleListScreen(
onNavigationEvent = {
when (it) {
is BleListContract.Effect.Navigation.BleFilter ->
controller.navigate(BleFilterRoute)
is BleListContract.Effect.Navigation.Thermometer ->
controller.navigate(ThermometerRoute(it.serial))
is BleListContract.Effect.Navigation.Gate ->
controller.navigate(GateRoute(it.serial))
is BleListContract.Effect.Navigation.Beacon ->
controller.navigate(BeaconRoute(it.serial))
is BleListContract.Effect.Navigation.Accelerometer ->
controller.navigate(AccelerometerRoute(it.serial))
}
}
)
}
composable(
route = "connection/{serial}",
content = {
ConnectionScreen(
onNavigationEvent = {
when(it){
ConnectionContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
is ConnectionContract.Effect.Navigation.NavigateToChangePassword ->
controller.navigate("change_password/${it.serial}")
is ConnectionContract.Effect.Navigation.NavigateToRotationsStatistic ->
controller.navigate("rotation_statistic/${it.serial}")
is ConnectionContract.Effect.Navigation.NavigateToThermometerHistory ->
controller.navigate(ThermometerHistoryRoute(it.bleSerial))
}
}
)
}
)
composable(
route = "rotation_statistic/{serial}",
content = {
RotationStatisticScreen {
when(it){
is RotationStatisticContract.Effect.Navigation.NavigateToDelete -> {
controller.navigate("rotation_delete/${it.serial}")
}
RotationStatisticContract.Effect.Navigation.NavigateUp -> {
controller.navigateUp()
}
}
}
}
)
dialog(
route = "rotation_delete/{serial}",
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
content = {
RotationDeleteScreen {
when(it){
RotationDeleteContract.Effect.Navigation.NavigateUp -> controller.navigateUp()
}
}
}
)
dialog(
route = "change_password/{serial}",
dialogProperties = DialogProperties(usePlatformDefaultWidth = false),
content = {
ChangePasswordScreen {
when(it){
is ChangePasswordContract.Effect.Navigation.Up -> controller.navigateUp()
}
}
}
)
}
)*/
} }

View File

@ -33,18 +33,14 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.navgraphs.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.password.view.Loading import llc.arma.ble.app.ui.screen.password.view.Loading
import llc.arma.ble.app.ui.screen.password.view.Result import llc.arma.ble.app.ui.screen.password.view.Result

View File

@ -2,6 +2,7 @@ package llc.arma.ble.app.ui.screen.password
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
@ -56,6 +57,8 @@ class ChangePasswordViewModel @Inject constructor(
event: ChangePasswordContract.Event.OnChange event: ChangePasswordContract.Event.OnChange
) { ) {
val params = ChangePasswordScreenDestination.argsFrom(savedStateHandle)
if(state.password.length != 6 || state.rePassword.length != 6){ if(state.password.length != 6 || state.rePassword.length != 6){
setState { setState {
state.copy( state.copy(
@ -83,7 +86,7 @@ class ChangePasswordViewModel @Inject constructor(
changeBlePassword.invoke( changeBlePassword.invoke(
state.password, state.password,
savedStateHandle.get<String>("serial")!! params.bleSerial
).fold( ).fold(
onSuccess = { onSuccess = {
setState { setState {

View File

@ -1,6 +1,5 @@
package llc.arma.ble.app.ui.theme package llc.arma.ble.app.ui.theme
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -9,12 +8,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
/*val LightColorScheme = lightColorScheme( /*val LightColorScheme = lightColorScheme(
primary = Color(0xFF1B1B1F), primary = Color(0xFF1B1B1F),

View File

@ -3,12 +3,10 @@ package llc.arma.ble.app.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import llc.arma.ble.R import llc.arma.ble.R
val font = FontFamily( val font = FontFamily(

View File

@ -2,6 +2,7 @@ package llc.arma.ble.data.repository
import android.Manifest import android.Manifest
import android.app.Application import android.app.Application
import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -9,13 +10,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import llc.arma.ble.data.db.RotationsDao import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.repository.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.fromByte import llc.arma.ble.data.repository.extensions.fromByte
@ -35,19 +39,13 @@ import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt import no.nordicsemi.kotlin.ble.client.RemoteService
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectOptions
import no.nordicsemi.android.kotlin.ble.core.scanner.BleNumOfMatches
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerCallbackType
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerMatchMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
import no.nordicsemi.kotlin.ble.client.android.CentralManager import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.Peripheral import no.nordicsemi.kotlin.ble.client.android.Peripheral
import no.nordicsemi.kotlin.ble.client.android.native import no.nordicsemi.kotlin.ble.client.android.native
import no.nordicsemi.kotlin.ble.core.WriteType
import java.lang.RuntimeException
import java.util.Collections import java.util.Collections
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
@ -56,8 +54,63 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.atan import kotlin.math.atan
import kotlin.random.Random
import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.ExperimentalUuidApi
suspend fun CentralManager.Factory.connectPeripheral(
serial: String,
context: Context,
scope: CoroutineScope
): Peripheral? {
val centralManager = CentralManager.Factory.native(context, scope)
return centralManager.connectPeripheral(serial)
}
suspend fun CentralManager.connectPeripheral(serial: String): Peripheral?{
CentralManager.Factory
val peripheral = getPeripheralById(serial)
return if(peripheral == null) {
null
} else {
withTimeoutOrNull(
timeMillis = 10000,
block = {
connect(
peripheral,
options = CentralManager.ConnectionOptions.AutoConnect,
)
return@withTimeoutOrNull peripheral
},
)
}
}
suspend fun RemoteCharacteristic.write(
data: ByteArray
) = write(data, WriteType.WITH_RESPONSE)
suspend fun StateFlow<List<RemoteService>>.firstNotEmptyOrNull() =
filter { it.isNotEmpty() }.firstOrNull()
@OptIn(ExperimentalUuidApi::class)
suspend fun Peripheral.discoverServices() = services().firstNotEmptyOrNull()
@OptIn(ExperimentalUuidApi::class)
fun List<RemoteService>.findService(uuid: UUID) =
firstOrNull { it.uuid.toString() == uuid.toString() }
@OptIn(ExperimentalUuidApi::class)
fun RemoteService.findCharacteristic(uuid: UUID) =
characteristics.firstOrNull { it.uuid.toString() == uuid.toString() }
@OptIn(ExperimentalUuidApi::class)
fun RemoteCharacteristic.findDescriptor(uuid: UUID) =
descriptors.firstOrNull { it.uuid.toString() == uuid.toString() }
val versionServiceUUID: UUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb") val versionServiceUUID: UUID = UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb")
val firmwareVersionUUID: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb") val firmwareVersionUUID: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
@ -89,25 +142,20 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(allOf = [ @RequiresPermission(allOf = [
android.Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
]) ])
override fun getBleAroundFlow() = callbackFlow { override fun getBleAroundFlow() = callbackFlow {
val job = BleScanner(app) val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
.scan( val job = centralManager.scan {
settings = BleScannerSettings( ManufacturerData(0x0059)
includeStoredBondedDevices = false, }
scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY, .filter { it.peripheral.name?.contains("ArmA") == true }
callbackType = BleScannerCallbackType.CALLBACK_TYPE_ALL_MATCHES,
matchMode = BleScannerMatchMode.MATCH_MODE_AGGRESSIVE,
numOfMatches = BleNumOfMatches.MATCH_NUM_ONE_ADVERTISEMENT,
reportDelay = 0L
)
).filter { it.device.name?.contains("ArmA") == true }
.onEach { .onEach {
resultList[it.device.address] = it.info resultList[it.peripheral.address] = it.info
}.launchIn(CoroutineScope(Dispatchers.IO)) }
.launchIn(CoroutineScope(Dispatchers.IO))
val timer = Timer().apply { val timer = Timer().apply {
schedule(object : TimerTask() { schedule(object : TimerTask() {
@ -158,6 +206,7 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override suspend fun getBleBySerial( override suspend fun getBleBySerial(
serial: String, serial: String,
@ -172,35 +221,13 @@ class BleRepositoryImpl @Inject constructor(
} else { } else {
println("Start")
val centralManager = CentralManager.Factory.native(app, scope) val centralManager = CentralManager.Factory.native(app, scope)
val peripheral = centralManager.getPeripheralById(serial) val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
if(peripheral!= null) {
withTimeout(10000) {
centralManager.connect(
peripheral,
options = CentralManager.ConnectionOptions.AutoConnect,
)
}
val version = peripheral.readVersion()
println("connected ${version}")
}
println("End")
var connection: ClientBleGatt? = null
try { try {
connection = ClientBleGatt.connect(app, serial, scope, BleGattConnectOptions(false)) val version = peripheral.readVersion()
val version = connection.readVersion()
fun BleInfo.updateState(): Ble.BleState { fun BleInfo.updateState(): Ble.BleState {
return Ble.BleState( return Ble.BleState(
@ -216,7 +243,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Gate( Ble.Gate(
info = initialBle, info = initialBle,
state = initialBle.updateState(), state = initialBle.updateState(),
gateState = connection.readHostState().fold( gateState = peripheral.readHostState().fold(
onFailure = { return Result.failure(it) }, onFailure = { return Result.failure(it) },
onSuccess = { it } onSuccess = { it }
) )
@ -236,7 +263,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Thermometer( Ble.Thermometer(
info = initialBle, info = initialBle,
state = initialBle.updateState(), state = initialBle.updateState(),
thermometerState = connection.readThermometerState( thermometerState = peripheral.readThermometerState(
initialBle.tableStatus initialBle.tableStatus
).fold( ).fold(
onFailure = { return Result.failure(it) }, onFailure = { return Result.failure(it) },
@ -251,7 +278,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Accelerometer( Ble.Accelerometer(
info = initialBle, info = initialBle,
state = initialBle.updateState(), state = initialBle.updateState(),
accelerometerState = connection.readAccelState( accelerometerState = peripheral.readAccelState(
initialBle.tableStatus, initialBle.tableStatus,
version version
).fold( ).fold(
@ -270,7 +297,7 @@ class BleRepositoryImpl @Inject constructor(
return Result.failure(BleException.UnexpectedResponse) return Result.failure(BleException.UnexpectedResponse)
} finally { } finally {
connection?.close() peripheral.disconnect()
} }
@ -280,21 +307,21 @@ class BleRepositoryImpl @Inject constructor(
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
private suspend fun ClientBleGatt.readThermometerState( private suspend fun Peripheral.readThermometerState(
timer: BleInfo.HistoryTableStatus timer: BleInfo.HistoryTableStatus
): Result<Ble.Thermometer.ThermometerState, BleException> { ): Result<Ble.Thermometer.ThermometerState, BleException> {
val service = discoverServices().findService(serviceUUID) val service = discoverServices()?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
var characteristic = service.findCharacteristic(temperatureReadUUID) var characteristic = service.findCharacteristic(temperatureReadUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write(DataByteArray.from(1, 1)) characteristic.write(byteArrayOf(1, 1))
delay(2_000) delay(2_000)
val temperature = characteristic.read().value.toUByteArray().toTemperature() val temperature = characteristic.read().toUByteArray().toTemperature()
characteristic = service.findCharacteristic(intervalReadUUID) characteristic = service.findCharacteristic(intervalReadUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
@ -312,11 +339,11 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readHostState( private suspend fun Peripheral.readHostState(
): Result<Ble.Gate.HostState, BleException> { ): Result<Ble.Gate.HostState, BleException> {
val service = discoverServices().findService(serviceUUID) val service = discoverServices()?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
val characteristic = service.findCharacteristic(intervalReadUUID) val characteristic = service.findCharacteristic(intervalReadUUID)
@ -335,11 +362,11 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGattCharacteristic.readWriteInterval(): Long { private suspend fun RemoteCharacteristic.readWriteInterval(): Long {
write(DataByteArray.from(3, 0, 0, 0)) write(byteArrayOf(3, 0, 0, 0))
return read().value.let { return read().let {
if(it.size == 4){ if(it.size == 4){
it.get4byteUIntAt(0).toLong() it.get4byteUIntAt(0).toLong()
} else { } else {
@ -350,11 +377,11 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGattCharacteristic.readTimeoutInterval(): Long { private suspend fun RemoteCharacteristic.readTimeoutInterval(): Long {
write(DataByteArray.from(8, 0, 0, 0)) write(byteArrayOf(8, 0, 0, 0))
return read().value.let { return read().let {
if (it.size == 4) { if (it.size == 4) {
it.get4byteUIntAt(0).toLong() it.get4byteUIntAt(0).toLong()
} else { } else {
@ -420,15 +447,12 @@ class BleRepositoryImpl @Inject constructor(
} }
@OptIn(ExperimentalUuidApi::class)
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun Peripheral.readVersion(): Version { private suspend fun Peripheral.readVersion(): Version {
return services().filter { it.isNotEmpty() }.firstOrNull() return discoverServices()
?.firstOrNull { it.uuid.toString() == versionServiceUUID.toString() } ?.findService(versionServiceUUID)
?.characteristics?.firstOrNull { it.uuid.toString() == firmwareVersionUUID.toString() } ?.findCharacteristic(firmwareVersionUUID)
?.read()?.decodeToString()?.let { ?.read()?.decodeToString()?.let {
Version.fromString(it) Version.fromString(it)
} ?: Version.fromString("0.0.0-0") } ?: Version.fromString("0.0.0-0")
@ -436,23 +460,13 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readVersion(): Version { private suspend fun Peripheral.readAccelState(
return discoverServices().findService(versionServiceUUID)
?.findCharacteristic(firmwareVersionUUID)?.read()?.value?.decodeToString()?.let {
Version.fromString(it)
} ?: Version.fromString("0.0.0-0")
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readAccelState(
timer: BleInfo.HistoryTableStatus, timer: BleInfo.HistoryTableStatus,
version: Version version: Version
): Result<Ble.Accelerometer.AccelerometerState, BleException> { ): Result<Ble.Accelerometer.AccelerometerState, BleException> {
val services = discoverServices() val services = discoverServices()
val service = services.findService(serviceUUID) val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
var characteristic = service.findCharacteristic(intervalReadUUID) var characteristic = service.findCharacteristic(intervalReadUUID)
@ -473,10 +487,8 @@ class BleRepositoryImpl @Inject constructor(
characteristic = service.findCharacteristic(accelerometerReadUUID) characteristic = service.findCharacteristic(accelerometerReadUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write(DataByteArray.from(4)) characteristic.write(byteArrayOf(4))
characteristic.read().let { characteristic.read().let { data ->
val data = it.value
val scale = AccelScale.fromByte(data[1]) ?: return Result.failure( val scale = AccelScale.fromByte(data[1]) ?: return Result.failure(
BleException.UnexpectedResponse BleException.UnexpectedResponse
@ -521,7 +533,7 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun getTemperatureHistoryBySerial( override suspend fun getTemperatureHistoryBySerial(
serial: String serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> { ): Result<List<Ble.Thermometer.HistoryPoint>, BleException> {
return readThermometerHistory(serial, app) return readThermometerHistory(serial, app)
@ -553,57 +565,52 @@ class BleRepositoryImpl @Inject constructor(
} }
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override suspend fun writeBle( override suspend fun writeBle(
serial: String, serial: String,
request: Ble.Thermometer.WriteRequest request: Ble.Thermometer.WriteRequest
): Result<Unit, BleException> { ): Result<Unit, BleException> {
return if(app.checkPermission()) {
val connection = val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default)) val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
try { return try {
val services = connection.discoverServices() val services = peripheral.discoverServices()
val service = services.findService(serviceUUID) ?: return Result.failure(BleException.UnexpectedResponse) val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
request.tx?.let { request.tx?.let {
service.findCharacteristic(txWriteUUID)!!.write( service.findCharacteristic(txWriteUUID)
DataByteArray.from(it.sendData) ?.write(byteArrayOf(it.sendData))
) ?: return Result.failure(BleException.UnexpectedResponse)
} }
request.saveHistory?.let { request.saveHistory?.let {
service.findCharacteristic(saveEnabledWriteUUID)!!.write( service.findCharacteristic(saveEnabledWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(4).apply {
*mutableListOf<Byte>(4).apply {
add(if (it) 1 else 0) add(if (it) 1 else 0)
}.toByteArray() }.toByteArray()
) )
)
} }
request.historyInterval?.let { request.historyInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write( service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(3).apply {
*mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()) addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray() }.toByteArray()
) )
)
} }
service.findCharacteristic(flashWriteUUID)!!.write( service.findCharacteristic(flashWriteUUID)!!.write(byteArrayOf(9))
DataByteArray.from(9)
)
connection.close()
peripheral.disconnect()
Result.success(Unit) Result.success(Unit)
} catch (err: Throwable) { } catch (err: Throwable) {
@ -613,13 +620,7 @@ class BleRepositoryImpl @Inject constructor(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
} }
@ -630,29 +631,28 @@ class BleRepositoryImpl @Inject constructor(
request: Ble.Beacon.WriteRequest request: Ble.Beacon.WriteRequest
): Result<Unit, BleException> { ): Result<Unit, BleException> {
return if(app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default)) return try {
try {
request.tx?.let { request.tx?.let {
connection.discoverServices().findService(serviceUUID)?.findCharacteristic( peripheral.discoverServices()
txWriteUUID ?.findService(serviceUUID)
)?.write( ?.findCharacteristic(txWriteUUID)
DataByteArray.from(it.sendData) ?.write(byteArrayOf(it.sendData)
) ?: return Result.failure(BleException.UnexpectedResponse) ) ?: return Result.failure(BleException.UnexpectedResponse)
} }
connection.discoverServices().findService(serviceUUID)?.findCharacteristic( peripheral.discoverServices()
flashWriteUUID ?.findService(serviceUUID)
)!!.write( ?.findCharacteristic(flashWriteUUID)!!
DataByteArray.from(9) .write(byteArrayOf(9))
)
connection.close() peripheral.disconnect()
Result.success(Unit) Result.success(Unit)
@ -663,15 +663,10 @@ class BleRepositoryImpl @Inject constructor(
} finally { } finally {
connection.close() peripheral.disconnect()
} }
} else {
Result.failure(BleException.PermissionDenied)
}
} }
@ -680,14 +675,15 @@ class BleRepositoryImpl @Inject constructor(
request: Ble.Gate.WriteRequest request: Ble.Gate.WriteRequest
): Result<Unit, BleException> { ): Result<Unit, BleException> {
return if(app.checkPermission()) {
val connection = ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default)) val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
try { return try {
val services = connection.discoverServices() val services = peripheral.discoverServices()
val service = services.findService(serviceUUID) ?: return Result.failure( val service = services?.findService(serviceUUID) ?: return Result.failure(
BleException.UnexpectedResponse BleException.UnexpectedResponse
) )
@ -696,7 +692,7 @@ class BleRepositoryImpl @Inject constructor(
service.findCharacteristic( service.findCharacteristic(
txWriteUUID txWriteUUID
)?.write( )?.write(
DataByteArray.from(it.sendData) byteArrayOf(it.sendData)
) ?: return Result.failure(BleException.UnexpectedResponse) ) ?: return Result.failure(BleException.UnexpectedResponse)
} }
@ -704,38 +700,30 @@ class BleRepositoryImpl @Inject constructor(
request.interval?.let { request.interval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write( service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(3).apply {
*mutableListOf<Byte>(3).apply { addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
}.toByteArray() }.toByteArray()
) )
)
} }
request.readInterval?.let { request.readInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write( service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(2).apply {
*mutableListOf<Byte>(2).apply { addAll((it / 1000).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
addAll(
(it / 1000).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
}.toByteArray() }.toByteArray()
) )
)
} }
service.findCharacteristic( service.findCharacteristic(
flashWriteUUID flashWriteUUID
)!!.write( )!!.write(
DataByteArray.from(9) byteArrayOf(9)
) )
connection.close() peripheral.disconnect()
Result.success(Unit) Result.success(Unit)
@ -745,13 +733,7 @@ class BleRepositoryImpl @Inject constructor(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
} }
@ -764,31 +746,27 @@ class BleRepositoryImpl @Inject constructor(
rotationsDao.deleteBySerial(serial) rotationsDao.deleteBySerial(serial)
return if(app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
val connection = return try {
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
try { val service = peripheral.discoverServices()?.findService(serviceUUID)
val services = connection.discoverServices()
val service = services.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
Log.d("write", request.toString()) Log.d("write", request.toString())
request.tx?.let { request.tx?.let {
service.findCharacteristic(txWriteUUID)!!.write( service.findCharacteristic(txWriteUUID)!!.write(
DataByteArray.from(it.sendData) byteArrayOf(it.sendData)
) )
} }
request.saveHistorySettings?.let { request.saveHistorySettings?.let {
service.findCharacteristic(saveEnabledWriteUUID)!!.write( service.findCharacteristic(saveEnabledWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(4).apply {
*mutableListOf<Byte>(4).apply {
add(if (it is Ble.Accelerometer.HistorySettings.Enabled) 1 else 0) add(if (it is Ble.Accelerometer.HistorySettings.Enabled) 1 else 0)
if (it is Ble.Accelerometer.HistorySettings.Enabled) { if (it is Ble.Accelerometer.HistorySettings.Enabled) {
add(it.mode.sendData) add(it.mode.sendData)
@ -796,40 +774,33 @@ class BleRepositoryImpl @Inject constructor(
} }
}.toByteArray() }.toByteArray()
) )
)
} }
request.historyInterval?.let { request.historyInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write( service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(3).apply {
*mutableListOf<Byte>(3).apply {
addAll( addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList() (it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
) )
}.toByteArray() }.toByteArray()
) )
)
} }
request.readInterval?.let { request.readInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write( service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from( mutableListOf<Byte>(2).apply {
*mutableListOf<Byte>(2).apply { addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
}.toByteArray() }.toByteArray()
) )
)
} }
service.findCharacteristic(flashWriteUUID)!!.write( service.findCharacteristic(flashWriteUUID)!!.write(
DataByteArray.from(9) byteArrayOf(9)
) )
Result.success(Unit) Result.success(Unit)
@ -841,13 +812,7 @@ class BleRepositoryImpl @Inject constructor(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
} }
@ -858,26 +823,24 @@ class BleRepositoryImpl @Inject constructor(
serial: String serial: String
): Result<Unit, BleException> { ): Result<Unit, BleException> {
return if(app.checkPermission()) {
val connection = val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default)) val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
try { return try {
val services = connection.discoverServices() val services = peripheral.discoverServices()
val service = services.findService(serviceUUID) val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse) ?: return Result.failure(BleException.UnexpectedResponse)
service.findCharacteristic(passwordWriteUUID)?.write( service.findCharacteristic(passwordWriteUUID)?.write(
DataByteArray(
mutableListOf(8.toByte()).apply { mutableListOf(8.toByte()).apply {
addAll(password.toByteArray(Charsets.US_ASCII).toList()) addAll(password.toByteArray(Charsets.US_ASCII).toList())
}.toByteArray() }.toByteArray()
)
) ?: return Result.failure(BleException.UnexpectedResponse) ) ?: return Result.failure(BleException.UnexpectedResponse)
connection.close() peripheral.disconnect()
Result.success(Unit) Result.success(Unit)
@ -888,24 +851,20 @@ class BleRepositoryImpl @Inject constructor(
} finally { } finally {
connection.close() peripheral.disconnect()
} }
} else {
Result.failure(BleException.PermissionDenied)
}
} }
override fun getAccelerometerMeasureBySerialFlow( override suspend fun getAccelerometerMeasureBySerialFlow(
serial: String, serial: String,
accelScale: AccelScale, accelScale: AccelScale,
accelMode: AccelViewMode, accelMode: AccelViewMode,
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency, frequency: FftFrequency,
): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> { ) : Result<Flow<Ble.Accelerometer.RealtimePoint>, BleException> {
return getAccelerometerRealtimeData(app, serial, accelScale, accelMode, fftAxis, fftMode, frequency) return getAccelerometerRealtimeData(app, serial, accelScale, accelMode, fftAxis, fftMode, frequency)

View File

@ -17,8 +17,8 @@ import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt import no.nordicsemi.kotlin.ble.client.android.native
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
@ -41,33 +41,31 @@ fun getAccelerometerHistory(
var expectedDataSize: Int? = null var expectedDataSize: Int? = null
if(app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Default))
val peripheral = centralManager.connectPeripheral(address) ?: throw IllegalStateException()
val connection =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
try { try {
val specData = connection.discoverServices() val specData = peripheral.discoverServices()
.findService(serviceUUID) ?.findService(serviceUUID)
?.findCharacteristic(accelerometerReadUUID) ?.findCharacteristic(accelerometerReadUUID)
?.let { ?.let {
it.write(DataByteArray.from(4)) it.write(byteArrayOf(4))
it.read() it.read()
} ?: throw IllegalStateException() } ?: throw IllegalStateException()
val scale = AccelScale.fromByte(specData.value[1]) ?: throw IllegalStateException() val scale = AccelScale.fromByte(specData[1]) ?: throw IllegalStateException()
val mode = AccelViewMode.fromByte(specData.value[0]) ?: throw IllegalStateException() val mode = AccelViewMode.fromByte(specData[0]) ?: throw IllegalStateException()
val characteristic = connection.discoverServices() val characteristic = peripheral.discoverServices()
.findService(serviceUUID) ?.findService(serviceUUID)
?.findCharacteristic(accelerometerHistoryReadUUID) ?.findCharacteristic(accelerometerHistoryReadUUID)
if (characteristic != null) { if (characteristic != null) {
characteristic.write(DataByteArray.from(2)) characteristic.write(byteArrayOf(2))
var value = characteristic.read().value var value = characteristic.read()
if (value.contentEquals(byteArrayOf(0, 0))) { if (value.contentEquals(byteArrayOf(0, 0))) {
@ -85,8 +83,8 @@ fun getAccelerometerHistory(
addAll(value.toList().take(2)) addAll(value.toList().take(2))
}.toByteArray() }.toByteArray()
characteristic.write(DataByteArray(writeData)) characteristic.write(writeData)
value = characteristic.read().value value = characteristic.read()
while (nextPackageDataCount.toInt() != 0) { while (nextPackageDataCount.toInt() != 0) {
@ -102,12 +100,16 @@ fun getAccelerometerHistory(
lastMeasureSystemTime = lastMeasureSystemTime =
System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000) System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
Log.d("dataTable", Log.d(
"dataTable",
"bleMeasureInterval $bleMeasureInterval " + "bleMeasureInterval $bleMeasureInterval " +
"bleLastMeasureTime $bleLastMeasureTime " + "bleLastMeasureTime $bleLastMeasureTime " +
"bleRealTime $bleRealTime " + "bleRealTime $bleRealTime " +
"lastMeasureSystemTime $lastMeasureSystemTime " + "lastMeasureSystemTime $lastMeasureSystemTime " +
"data size ${value.toUByteArray().asList().subList(16, value.size).size}" "data size ${
value.toUByteArray().asList()
.subList(16, value.size).size
}"
) )
value.toUByteArray().asList().subList(16, value.size) value.toUByteArray().asList().subList(16, value.size)
@ -133,8 +135,8 @@ fun getAccelerometerHistory(
emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat()))) emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
emit(Result.success(ProgressState.Progress(resultPackage.size.toFloat() / expectedDataSize.toFloat()))) emit(Result.success(ProgressState.Progress(resultPackage.size.toFloat() / expectedDataSize.toFloat())))
characteristic.write(DataByteArray.from(5)) characteristic.write(byteArrayOf(5))
value = characteristic.read().value value = characteristic.read()
} }
@ -150,9 +152,11 @@ fun getAccelerometerHistory(
) )
} }
} }
AccelViewMode.ACCELERATION, AccelViewMode.ACCELERATION,
AccelViewMode.PEAK_ACCELERATION, AccelViewMode.PEAK_ACCELERATION,
AccelViewMode.RMS -> { AccelViewMode.RMS
-> {
resultPackage.chunked(3).withIndex().map { resultPackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle( Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultPackage.size / 3 - 1) - it.index) * bleMeasureInterval!!), date = lastMeasureSystemTime!! - (((resultPackage.size / 3 - 1) - it.index) * bleMeasureInterval!!),
@ -214,14 +218,10 @@ fun getAccelerometerHistory(
} finally { } finally {
connection.close() peripheral.disconnect()
} }
} else {
emit(Result.failure(BleException.PermissionDenied))
}
} }

View File

@ -5,6 +5,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onEmpty
import llc.arma.ble.data.repository.extensions.checkPermission import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.get2byteShortAt import llc.arma.ble.data.repository.extensions.get2byteShortAt
import llc.arma.ble.data.repository.extensions.sendData import llc.arma.ble.data.repository.extensions.sendData
@ -22,10 +27,10 @@ import llc.arma.ble.domain.usecase.AccelViewMode.VIBRATION
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt import kotlin.uuid.ExperimentalUuidApi
fun getAccelerometerRealtimeData( suspend fun getAccelerometerRealtimeData(
app: Application, app: Application,
serial: String, serial: String,
accelScale: AccelScale, accelScale: AccelScale,
@ -33,26 +38,23 @@ fun getAccelerometerRealtimeData(
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency, frequency: FftFrequency,
): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> { ): Result<Flow<Ble.Accelerometer.RealtimePoint>, BleException> {
return flow { val peripheral = CentralManager.Factory.connectPeripheral(serial, app, CoroutineScope(Dispatchers.Default))
if(app.checkPermission()) { return try {
val connection = if (peripheral == null) throw IllegalStateException()
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
try { val services = peripheral.discoverServices()
val services = connection.discoverServices()
val characteristic = val characteristic =
services.findService(serviceUUID) services?.findService(serviceUUID)
?.findCharacteristic(accelerometerReadUUID) ?.findCharacteristic(accelerometerReadUUID)
?: throw IllegalStateException() ?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write( characteristic.write(
DataByteArray.from( byteArrayOf(
4, 4,
accelMode.sendData, accelMode.sendData,
accelScale.sendData, accelScale.sendData,
@ -63,9 +65,13 @@ fun getAccelerometerRealtimeData(
) )
) )
characteristic.getNotifications().collect { val flow = characteristic.subscribe()
.onCompletion {
val value = it.value println("disable notifying")
characteristic.setNotifying(false)
peripheral.disconnect()
}
.map { value ->
val data = value.toList().chunked(2).map { val data = value.toList().chunked(2).map {
it.toByteArray().get2byteShortAt() it.toByteArray().get2byteShortAt()
@ -78,6 +84,7 @@ fun getAccelerometerRealtimeData(
.toFloat() * accelScale.k) / Short.MAX_VALUE .toFloat() * accelScale.k) / Short.MAX_VALUE
) )
} }
ANGLE -> { ANGLE -> {
Ble.Accelerometer.RealtimePoint.Angle( Ble.Accelerometer.RealtimePoint.Angle(
x = calculateAngle( x = calculateAngle(
@ -94,6 +101,7 @@ fun getAccelerometerRealtimeData(
) )
) )
} }
ROTATIONS -> { ROTATIONS -> {
Ble.Accelerometer.RealtimePoint.Rotation( Ble.Accelerometer.RealtimePoint.Rotation(
angle = ((360f / 8f) * ((data[0] / 100f) + 1f)) - 45f, angle = ((360f / 8f) * ((data[0] / 100f) + 1f)) - 45f,
@ -101,6 +109,7 @@ fun getAccelerometerRealtimeData(
turnovers = data[2] turnovers = data[2]
) )
} }
ACCELERATION, ACCELERATION,
PEAK_ACCELERATION, PEAK_ACCELERATION,
RMS -> { RMS -> {
@ -112,29 +121,20 @@ fun getAccelerometerRealtimeData(
} }
} }
emit(Result.success(result)) result
} }
Result.success(flow)
} catch (err: Exception) { } catch (err: Exception) {
peripheral?.disconnect()
err.printStackTrace() err.printStackTrace()
emit(Result.failure(BleException.UnexpectedResponse)) Result.failure(BleException.UnexpectedResponse)
} finally {
connection.disconnect()
connection.close()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
}
} }

View File

@ -20,8 +20,7 @@ import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import java.util.UUID import java.util.UUID
fun readAccelerometerSpectre( fun readAccelerometerSpectre(
@ -36,15 +35,17 @@ fun readAccelerometerSpectre(
return flow { return flow {
if(app.checkPermission()) { val peripheral =
CentralManager.Factory.connectPeripheral(
val connection = address,
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default)) app,
CoroutineScope(Dispatchers.Default)
) ?: throw IllegalStateException()
try { try {
val service = connection.discoverServices() val service = peripheral.discoverServices()
.findService(serviceUUID) ?: throw IllegalStateException() ?.findService(serviceUUID) ?: throw IllegalStateException()
val characteristic = service.findCharacteristic(accelerometerReadUUID) val characteristic = service.findCharacteristic(accelerometerReadUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
@ -52,13 +53,15 @@ fun readAccelerometerSpectre(
val historyCharacteristic = service.findCharacteristic(accelerometerHistoryReadUUID) val historyCharacteristic = service.findCharacteristic(accelerometerHistoryReadUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
characteristic.findDescriptor( characteristic.setNotifying(true)
/*characteristic.findDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
)?.write(DataByteArray(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) )?.write(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
?: throw IllegalStateException() ?: throw IllegalStateException()*/
characteristic.write( characteristic.write(
DataByteArray(byteArrayOf( byteArrayOf(
4, 4,
accelMode.sendData, accelMode.sendData,
accelScale.sendData, accelScale.sendData,
@ -66,10 +69,11 @@ fun readAccelerometerSpectre(
fftAxis.sendData, fftAxis.sendData,
frequency.sendData, frequency.sendData,
2 2
))
) )
characteristic.getNotifications().collect { )
characteristic.subscribe().collect {
var initialValue: Long? = null var initialValue: Long? = null
var frequencyInterval: Long? = null var frequencyInterval: Long? = null
@ -78,9 +82,9 @@ fun readAccelerometerSpectre(
var expectedDataSize: Int? = null var expectedDataSize: Int? = null
historyCharacteristic.write(DataByteArray.from(2)) historyCharacteristic.write(byteArrayOf(2))
var value = historyCharacteristic.read().value var value = historyCharacteristic.read()
if (value.contentEquals(byteArrayOf(0, 0))) { if (value.contentEquals(byteArrayOf(0, 0))) {
@ -98,8 +102,8 @@ fun readAccelerometerSpectre(
addAll(value.toList()) addAll(value.toList())
}.toByteArray() }.toByteArray()
historyCharacteristic.write(DataByteArray(writeData)) historyCharacteristic.write(writeData)
value = historyCharacteristic.read().value value = historyCharacteristic.read()
while (nextPackageDataCount.toInt() != 0) { while (nextPackageDataCount.toInt() != 0) {
@ -124,13 +128,14 @@ fun readAccelerometerSpectre(
}.toMutableList() }.toMutableList()
) )
expectedDataSize = nextPackageDataCount.toInt() + resultAccelerometerPackage.size expectedDataSize =
nextPackageDataCount.toInt() + resultAccelerometerPackage.size
emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat()))) emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
emit(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize.toFloat()))) emit(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize.toFloat())))
historyCharacteristic.write(DataByteArray.from(5)) historyCharacteristic.write(byteArrayOf(5))
value = historyCharacteristic.read().value value = historyCharacteristic.read()
} }
@ -148,7 +153,8 @@ fun readAccelerometerSpectre(
) )
characteristic.write(DataByteArray(byteArrayOf( characteristic.write(
byteArrayOf(
4, 4,
accelMode.sendData, accelMode.sendData,
accelScale.sendData, accelScale.sendData,
@ -156,7 +162,9 @@ fun readAccelerometerSpectre(
fftAxis.sendData, fftAxis.sendData,
frequency.sendData, frequency.sendData,
2 2
))) )
)
} }
@ -170,18 +178,10 @@ fun readAccelerometerSpectre(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
} }
} }
} }

View File

@ -2,35 +2,32 @@ package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import androidx.annotation.RequiresPermission
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
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.get2byteUIntAt
import llc.arma.ble.data.repository.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic import no.nordicsemi.kotlin.ble.client.android.native
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.BitSet import java.util.BitSet
import java.util.Locale import java.util.Locale
@RequiresPermission(allOf = ["android.permission.BLUETOOTH_CONNECT"])
suspend fun readTable( suspend fun readTable(
characteristic: ClientBleGattCharacteristic, characteristic: RemoteCharacteristic,
startRequest: ByteArray, startRequest: ByteArray,
nextRequestPayload: ByteArray nextRequestPayload: ByteArray
): List<Byte> { ): List<Byte> {
characteristic.write(DataByteArray(startRequest)) characteristic.write(startRequest)
var value = characteristic.read().value var value = characteristic.read()
val tableResult = mutableListOf<Byte>() val tableResult = mutableListOf<Byte>()
do { do {
@ -44,8 +41,8 @@ suspend fun readTable(
tableResult.addAll(value.asList().subList(4, value.size)) tableResult.addAll(value.asList().subList(4, value.size))
characteristic.write(DataByteArray(nextRequestPayload)) characteristic.write(nextRequestPayload)
value = characteristic.read().value value = characteristic.read()
} }
@ -63,21 +60,21 @@ fun readHostHistory(
return flow { return flow {
if (app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Main))
val connection = val peripheral = centralManager.connectPeripheral(address)
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default)) ?: throw IllegalStateException()
try { try {
val characteristic = connection.discoverServices() val characteristic = peripheral?.discoverServices()
.findService(serviceUUID) ?.findService(serviceUUID)
?.findCharacteristic(hostHistoryReadUUID) ?.findCharacteristic(hostHistoryReadUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
characteristic.write(DataByteArray.from(2)) characteristic.write(byteArrayOf(2))
var value = characteristic.read().value var value = characteristic.read()
if (value.contentEquals(byteArrayOf(0, 0))) { if (value.contentEquals(byteArrayOf(0, 0))) {
@ -91,8 +88,8 @@ fun readHostHistory(
var tableSize = value.get2byteUIntAt(0) var tableSize = value.get2byteUIntAt(0)
//Чтение без удаления //Чтение без удаления
characteristic.write(DataByteArray.from(1, 0, 0, -1, -1)) characteristic.write(byteArrayOf(1, 0, 0, -1, -1))
val firstTableHeader = characteristic.read().value.asList() val firstTableHeader = characteristic.read().asList()
dataTablePackage.addAll(firstTableHeader.subList(4, firstTableHeader.size)) dataTablePackage.addAll(firstTableHeader.subList(4, firstTableHeader.size))
secondTablePackage.addAll( secondTablePackage.addAll(
@ -112,9 +109,13 @@ fun readHostHistory(
val bleMeasureInterval = dataTablePackage.toByteArray().get4byteUIntAt(0).toLong() val bleMeasureInterval = dataTablePackage.toByteArray().get4byteUIntAt(0).toLong()
val bleLastMeasureTime = dataTablePackage.toByteArray().get4byteUIntAt(4).toLong() val bleLastMeasureTime = dataTablePackage.toByteArray().get4byteUIntAt(4).toLong()
val bleRealTime = dataTablePackage.toByteArray().get4byteUIntAt(8).toLong() val bleRealTime = dataTablePackage.toByteArray().get4byteUIntAt(8).toLong()
val lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000) val lastMeasureSystemTime =
System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
Log.i("BLEK-LOG", "Вермя последнего: ${bleLastMeasureTime} Реальное время: ${bleRealTime}") Log.i(
"BLEK-LOG",
"Вермя последнего: ${bleLastMeasureTime} Реальное время: ${bleRealTime}"
)
fun getBleIdIndex(bytes: ByteArray): UInt { fun getBleIdIndex(bytes: ByteArray): UInt {
@ -235,7 +236,10 @@ fun readHostHistory(
dataTablePackage.drop(bleTableOffset).take(devDataSize) dataTablePackage.drop(bleTableOffset).take(devDataSize)
.toByteArray() .toByteArray()
//) //)
Log.d("payload", "${payload.joinToString(separator = " ")} ${payload.toHexString()}") Log.d(
"payload",
"${payload.joinToString(separator = " ")} ${payload.toHexString()}"
)
bleTableOffset += devDataSize bleTableOffset += devDataSize
} }
@ -267,13 +271,7 @@ fun readHostHistory(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
} }
@ -287,21 +285,20 @@ suspend fun readHostBleTable(
app: Application, app: Application,
): Result<List<String>, BleException> { ): Result<List<String>, BleException> {
return if (app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Main))
val peripheral = centralManager.connectPeripheral(address)
?: return Result.failure(BleException.UnexpectedResponse)
val connection = return try {
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
try { val characteristic = peripheral.discoverServices()
?.findService(serviceUUID)
val characteristic = connection.discoverServices()
.findService(serviceUUID)
?.findCharacteristic(hostHistoryReadUUID) ?.findCharacteristic(hostHistoryReadUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
characteristic.write(DataByteArray.from(7)) characteristic.write(byteArrayOf(7))
var value = characteristic.read().value var value = characteristic.read()
if (value.contentEquals(byteArrayOf(0, 0))) { if (value.contentEquals(byteArrayOf(0, 0))) {
@ -311,16 +308,8 @@ suspend fun readHostBleTable(
var tableSize = value.get2byteUIntAt(0) var tableSize = value.get2byteUIntAt(0)
val writeData = mutableListOf( characteristic.write(byteArrayOf(1, 0, 0) + value)
1.toByte(), value = characteristic.read()
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
characteristic.write(DataByteArray(writeData))
value = characteristic.read().value
Result.success( Result.success(
readTable(characteristic, byteArrayOf(6), byteArrayOf(6)).chunked(8).map { readTable(characteristic, byteArrayOf(6), byteArrayOf(6)).chunked(8).map {
@ -342,13 +331,7 @@ suspend fun readHostBleTable(
} finally { } finally {
connection.close() peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
} }
@ -361,25 +344,25 @@ suspend fun editBleHostTable(
app: Application, app: Application,
): Result<Int, BleException> { ): Result<Int, BleException> {
return if (app.checkPermission()) { val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Default))
val peripheral = centralManager.connectPeripheral(address) ?: throw IllegalStateException()
val connection = return try {
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
try { val characteristic = peripheral.discoverServices()
?.findService(serviceUUID)
val characteristic = connection.discoverServices()
.findService(serviceUUID)
?.findCharacteristic(flashWriteUUID) ?.findCharacteristic(flashWriteUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
characteristic.write(DataByteArray.from(12, 1)) characteristic.write(byteArrayOf(12, 1))
Log.i("ScanRecord", "write") Log.i("ScanRecord", "write")
val writeCount = addBleAddress.chunked(20).sumOf { bleAddressBatch -> val writeCount = addBleAddress.chunked(20).sumOf { bleAddressBatch ->
val countPayload = ByteBuffer.allocate(2).putShort(bleAddressBatch.size.toShort()).array().reversed().toByteArray() val countPayload =
ByteBuffer.allocate(2).putShort(bleAddressBatch.size.toShort()).array().reversed()
.toByteArray()
val command = "0b00".hexToByteArray() val command = "0b00".hexToByteArray()
@ -387,14 +370,12 @@ suspend fun editBleHostTable(
it.replace(":", "").lowercase(Locale.CANADA).hexToByteArray().reversed().toList() it.replace(":", "").lowercase(Locale.CANADA).hexToByteArray().reversed().toList()
}.toByteArray() }.toByteArray()
characteristic.write(DataByteArray.from(*command, *countPayload, *serialPayload)) characteristic.write(byteArrayOf(*command, *countPayload, *serialPayload))
characteristic.read().value.get2byteUIntAt(0).toInt() characteristic.read().get2byteUIntAt(0).toInt()
} }
characteristic.write( characteristic.write(byteArrayOf(9))
DataByteArray.from(9)
)
delay(10_000) delay(10_000)
@ -408,14 +389,9 @@ suspend fun editBleHostTable(
} finally { } finally {
connection.close() peripheral.disconnect()
} }
} else {
Result.failure(BleException.PermissionDenied)
}
} }

View File

@ -3,26 +3,19 @@ package llc.arma.ble.data.repository
import android.app.Application import android.app.Application
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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.get2byteUIntAt
import llc.arma.ble.data.repository.extensions.get4byteUIntAt import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.repository.extensions.toTemperature import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException 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.Ble
import no.nordicsemi.android.common.core.DataByteArray import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
fun readThermometerHistory( suspend fun readThermometerHistory(
address: String, address: String,
app: Application, app: Application,
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> { ): Result<List<Ble.Thermometer.HistoryPoint>, BleException> {
return flow {
var lastMeasureSystemTime: Long? = null var lastMeasureSystemTime: Long? = null
@ -34,25 +27,27 @@ fun readThermometerHistory(
var expectedDataSize: Int? = null var expectedDataSize: Int? = null
if (app.checkPermission()) { val peripheral =
CentralManager.Factory.connectPeripheral(
address,
app,
CoroutineScope(Dispatchers.Default)
)
val connection = return try {
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
try { val characteristic = peripheral?.discoverServices()
?.findService(serviceUUID)
val characteristic = connection.discoverServices()
.findService(serviceUUID)
?.findCharacteristic(temperatureHistoryReadUUID) ?.findCharacteristic(temperatureHistoryReadUUID)
?: throw IllegalStateException() ?: throw IllegalStateException()
characteristic.write(DataByteArray.from(2)) characteristic.write(byteArrayOf(2))
var value = characteristic.read().value var value = characteristic.read()
if (value.contentEquals(byteArrayOf(0, 0))) { if (value.contentEquals(byteArrayOf(0, 0))) {
emit(Result.success(ProgressState.Finished(emptyList()))) Result.success(emptyList())
} else { } else {
@ -66,8 +61,8 @@ fun readThermometerHistory(
addAll(value.toList()) addAll(value.toList())
}.toByteArray() }.toByteArray()
characteristic.write(DataByteArray(writeData)) characteristic.write(writeData)
value = characteristic.read().value value = characteristic.read()
while (nextPackageDataCount.toInt() != 0) { while (nextPackageDataCount.toInt() != 0) {
@ -99,17 +94,13 @@ fun readThermometerHistory(
expectedDataSize = expectedDataSize =
nextPackageDataCount.toInt() + resultTemperaturePackage.size nextPackageDataCount.toInt() + resultTemperaturePackage.size
emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat()))) characteristic.write(byteArrayOf(5))
emit(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize.toFloat()))) value = characteristic.read()
characteristic.write(DataByteArray.from(5))
value = characteristic.read().value
} }
emit(
Result.success( Result.success(
ProgressState.Finished(
resultTemperaturePackage.withIndex().map { resultTemperaturePackage.withIndex().map {
Ble.Thermometer.HistoryPoint( Ble.Thermometer.HistoryPoint(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!), date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
@ -117,28 +108,20 @@ fun readThermometerHistory(
) )
} }
) )
)
)
} }
} catch (err: Throwable) { } catch (err: Throwable) {
emit(Result.failure(BleException.UnexpectedResponse)) err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally { } finally {
connection.close() peripheral?.disconnect()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
}
} }

View File

@ -8,7 +8,7 @@ import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
fun Ble.BleState.TX.Companion.fromByte(byte: Byte): Ble.BleState.TX? { fun Ble.BleState.TX.Companion.fromByte(byte: Byte): Ble.BleState.TX? {
return Ble.BleState.TX.values().associateBy { it.sendData }[byte] return Ble.BleState.TX.entries.associateBy { it.sendData }[byte]
} }
val Ble.BleState.TX.sendData: Byte val Ble.BleState.TX.sendData: Byte
@ -63,7 +63,7 @@ val FftViewMode.sendData: Byte
} }
fun AccelViewMode.Companion.fromByte(byte: Byte): AccelViewMode? { fun AccelViewMode.Companion.fromByte(byte: Byte): AccelViewMode? {
return AccelViewMode.values().associateBy { it.sendData }[byte] return AccelViewMode.entries.associateBy { it.sendData }[byte]
} }
val AccelViewMode.sendData: Byte val AccelViewMode.sendData: Byte
@ -79,7 +79,7 @@ val AccelViewMode.sendData: Byte
} }
fun AccelScale.Companion.fromByte(byte: Byte): AccelScale? { fun AccelScale.Companion.fromByte(byte: Byte): AccelScale? {
return AccelScale.values().associateBy { it.sendData }[byte] return AccelScale.entries.associateBy { it.sendData }[byte]
} }
val AccelScale.sendData: Byte val AccelScale.sendData: Byte

View File

@ -1,52 +1,46 @@
package llc.arma.ble.data.repository.extensions package llc.arma.ble.data.repository.extensions
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanResult import no.nordicsemi.kotlin.ble.client.android.ScanResult
val BleScanResult.timerEnabled: BleInfo.HistoryTableStatus val ScanResult.timerEnabled: BleInfo.HistoryTableStatus
get() { get() {
return when(data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(2)){
return when(advertisingData.manufacturerData[89]?.get(2)){
1.toByte() -> BleInfo.HistoryTableStatus.NOT_EMPTY 1.toByte() -> BleInfo.HistoryTableStatus.NOT_EMPTY
2.toByte() -> BleInfo.HistoryTableStatus.EMPTY 2.toByte() -> BleInfo.HistoryTableStatus.EMPTY
else -> BleInfo.HistoryTableStatus.DISABLED else -> BleInfo.HistoryTableStatus.DISABLED
} }
} }
val BleScanResult.info: BleInfo val ScanResult.info: BleInfo
get() { get() {
this.device.name this.timestamp
return BleInfo( return BleInfo(
name = this.device.name ?: "", name = peripheral.name ?: "",
serial = device.address, serial = peripheral.address,
batteryLevel = batteryLevel ?: 0, batteryLevel = batteryLevel ?: 0,
rssi = data?.rssi, rssi = rssi,
type = type, type = type,
scanTime = (data?.timestampNanos ?: 0) / 1_000_000, scanTime = (timestamp ?: 0) / 1_000,
tx = data?.scanRecord?.txPowerLevel ?: 0, tx = advertisingData.txPowerLevel ?: 0,
tableStatus = timerEnabled tableStatus = timerEnabled
) )
} }
val BleScanResult.batteryLevel: Int? val ScanResult.batteryLevel: Int?
get() { get() {
val level = data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(1) val level = advertisingData.manufacturerData[89]?.get(1)
?.toUByte()?.toInt() ?.toUByte()?.toInt()
if(data?.scanRecord?.deviceName?.contains("N00051023") == true){
println(level)
println(data?.scanRecord?.manufacturerSpecificData?.get(89))
}
return level return level
} }
val BleScanResult.type: BleInfo.Type val ScanResult.type: BleInfo.Type
get() { get() {
return when(data?.scanRecord?.manufacturerSpecificData?.get(89)?.getByte(0)?.toUByte()?.toInt()){ return when(advertisingData.manufacturerData[89]?.get(0)?.toUByte()?.toInt()){
4 -> BleInfo.Type.HOST 4 -> BleInfo.Type.HOST
1 -> BleInfo.Type.BEACON 1 -> BleInfo.Type.BEACON
2 -> BleInfo.Type.THERMOMETER 2 -> BleInfo.Type.THERMOMETER

View File

@ -1,6 +1,8 @@
package llc.arma.ble.domain.model package llc.arma.ble.domain.model
import kotlinx.serialization.Serializable
import llc.arma.ble.data.repository.BleRepositoryImpl import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.Ble.Gate.HostState
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode import llc.arma.ble.domain.usecase.AccelViewMode
@ -15,18 +17,22 @@ sealed class Ble(
val accelerometerState: AccelerometerState val accelerometerState: AccelerometerState
): Ble(info, state) { ): Ble(info, state) {
@Serializable
sealed class HistorySettings { sealed class HistorySettings {
@Serializable
data class Enabled( data class Enabled(
val scale: AccelScale, val scale: AccelScale,
val mode: AccelViewMode, val mode: AccelViewMode,
val detailed: Boolean val detailed: Boolean
) : HistorySettings() ) : HistorySettings()
@Serializable
data object Disabled : HistorySettings() data object Disabled : HistorySettings()
} }
@Serializable
data class WriteRequest( data class WriteRequest(
val tx: BleState.TX?, val tx: BleState.TX?,
val saveHistorySettings: HistorySettings?, val saveHistorySettings: HistorySettings?,
@ -99,6 +105,26 @@ sealed class Ble(
val readInterval: Long val readInterval: Long
) )
fun copy(
info: BleInfo = this.info,
state: BleState = this.state,
accelerometerState: AccelerometerState = this.accelerometerState
): Accelerometer {
return Accelerometer(info, state, accelerometerState)
}
override fun equals(other: Any?): Boolean {
return if(other is Accelerometer){
info == other.info && state == other.state && accelerometerState == other.accelerometerState
}else{
false
}
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
} }
class Beacon( class Beacon(
@ -106,10 +132,30 @@ sealed class Ble(
state: BleState, state: BleState,
) : Ble(info, state){ ) : Ble(info, state){
@Serializable
data class WriteRequest( data class WriteRequest(
val tx: BleState.TX? val tx: BleState.TX?
) )
fun copy(
info: BleInfo = this.info,
state: BleState = this.state,
): Beacon {
return Beacon(info, state)
}
override fun equals(other: Any?): Boolean {
return if(other is Beacon){
info == other.info && state == other.state
} else {
false
}
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
} }
class Gate( class Gate(
@ -129,12 +175,33 @@ sealed class Ble(
val readInterval: Long val readInterval: Long
) )
@Serializable
data class WriteRequest( data class WriteRequest(
val tx: BleState.TX?, val tx: BleState.TX?,
val interval: Long?, val interval: Long?,
val readInterval: Long?, val readInterval: Long?,
) )
fun copy(
info: BleInfo = this.info,
state: BleState = this.state,
gateState: HostState = this.gateState
): Gate {
return Gate(info, state, gateState)
}
override fun equals(other: Any?): Boolean {
return if(other is Gate){
info == other.info && state == other.state && gateState == other.gateState
}else{
false
}
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
} }
class Thermometer( class Thermometer(
@ -154,12 +221,33 @@ sealed class Ble(
val historyInterval: Long val historyInterval: Long
) )
@Serializable
data class WriteRequest( data class WriteRequest(
val tx: BleState.TX?, val tx: BleState.TX?,
val saveHistory: Boolean?, val saveHistory: Boolean?,
val historyInterval: Long? val historyInterval: Long?
) )
fun copy(
info: BleInfo = this.info,
state: BleState = this.state,
thermometerState: ThermometerState = this.thermometerState
): Thermometer {
return Thermometer(info, state, thermometerState)
}
override fun equals(other: Any?): Boolean {
return if(other is Thermometer){
info == other.info && state == other.state && thermometerState == other.thermometerState
}else{
false
}
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
} }
data class BleState( data class BleState(

View File

@ -1,5 +1,8 @@
package llc.arma.ble.domain.model package llc.arma.ble.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class BleName( data class BleName(
val serial: String, val serial: String,
val name: String val name: String

View File

@ -30,7 +30,7 @@ interface BleRepository {
suspend fun getTemperatureHistoryBySerial( suspend fun getTemperatureHistoryBySerial(
serial: String serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> ): Result<List<Ble.Thermometer.HistoryPoint>, BleException>
suspend fun writeBle( suspend fun writeBle(
serial: String, serial: String,
@ -57,14 +57,14 @@ interface BleRepository {
serial: String serial: String
): Result<Unit, BleException> ): Result<Unit, BleException>
fun getAccelerometerMeasureBySerialFlow( suspend fun getAccelerometerMeasureBySerialFlow(
serial: String, serial: String,
accelScale: AccelScale, accelScale: AccelScale,
accelMode: AccelViewMode, accelMode: AccelViewMode,
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency frequency: FftFrequency
): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> ): Result<Flow<Ble.Accelerometer.RealtimePoint>, BleException>
suspend fun getAccelerometerSpectreBySerial( suspend fun getAccelerometerSpectreBySerial(
serial: String, serial: String,

View File

@ -11,14 +11,14 @@ class GetAccelerometerMeasureBySerialFlow @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository
) { ) {
operator fun invoke( suspend operator fun invoke(
serial: String, serial: String,
accelScale: AccelScale, accelScale: AccelScale,
accelMode: AccelViewMode, accelMode: AccelViewMode,
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency frequency: FftFrequency
): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> { ): Result<Flow<Ble.Accelerometer.RealtimePoint>, BleException> {
return bleRepository.getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency) return bleRepository.getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency)

View File

@ -12,7 +12,9 @@ class GetTemperatureHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository private val bleRepository: BleRepository
) { ) {
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> { suspend operator fun invoke(
serial: String
): Result<List<Ble.Thermometer.HistoryPoint>, BleException> {
return bleRepository.getTemperatureHistoryBySerial(serial) return bleRepository.getTemperatureHistoryBySerial(serial)

View File

@ -45,4 +45,6 @@ dependencies {
implementation(libs.scanner) implementation(libs.scanner)
implementation(libs.client) implementation(libs.client)
implementation("no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha02")
} }

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