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>
<SelectionState runConfigName="app">
<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">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=BV5900DNS00004122" />

View File

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

View File

@ -7,70 +7,45 @@ import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Bundle
import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
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.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BottomState
import llc.arma.ble.app.ui.common.LocalBottomDialogState
import llc.arma.ble.app.ui.screen.main.MainScreen
import llc.arma.ble.app.ui.theme.BleTheme
import org.slf4j.simple.SimpleLogger.DEFAULT_LOG_LEVEL_KEY
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
init {
System.setProperty(DEFAULT_LOG_LEVEL_KEY, "Debug")
}
@SuppressLint("MissingPermission")
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -78,102 +53,12 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
//installSplashScreen()
installSplashScreen()
setContent {
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 ->
Surface(
modifier = Modifier
@ -220,7 +105,7 @@ class MainActivity : ComponentActivity() {
mutableStateOf(mBluetoothAdapter.isEnabled)
}
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
class BleListContract {
@ -22,8 +21,18 @@ class BleListContract {
data class State(
val bleList: List<BleInfo>,
val bleFilter: BleFilter
) : ViewState
val filterIsEmpty: Boolean,
val summary: BleSummary
) : ViewState {
data class BleSummary(
val all: Int,
val lowBattery: Int,
val lost: Int,
val active: Int
)
}
sealed class Effect : ViewSideEffect {

View File

@ -1,7 +1,6 @@
package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -20,17 +19,21 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.Battery1Bar
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Link
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Summarize
import androidx.compose.material.icons.rounded.TimerOff
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
@ -38,21 +41,22 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.BleFilterScreenDestination
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.navigation.DestinationsNavigator
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.icon
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
@Destination<RootGraph>(start = true)
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BleListScreen(
//onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
@ -131,47 +130,16 @@ fun BleListScreen(
},
actions = {
Row(
modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
) {
Text(text = "${state.bleList.size}")
Spacer(modifier = Modifier.width(12.dp))
Text(text = "${state.bleList.filter {
it.batteryLevel == 100
}.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
Text(text = " | ")
Text(
text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
Text(text = " | ")
Text(
text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
color = MaterialTheme.colorScheme.error
)
}
IconButton(
onClick = {
FilledIconToggleButton(
checked = state.filterIsEmpty.not(),
onCheckedChange = {
viewModel.setEvent(BleListContract.Event.OnShowFilter)
}
) {
Icon(
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
}
}
@ -183,39 +151,9 @@ fun BleListScreen(
modifier = Modifier.padding(it)
) {
val filteredData = remember(state.bleList, state.bleFilter) {
var showSummary by remember { mutableStateOf(false) }
state.bleList.filter {
(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()){
if(state.bleList.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
@ -224,7 +162,7 @@ fun BleListScreen(
)
}
if(filteredData.isEmpty()){
if(state.bleList.isEmpty()){
Box(modifier = Modifier.fillMaxSize()){
Text(
@ -242,14 +180,130 @@ fun BleListScreen(
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 = filteredData,
items = state.bleList,
key = { it.serial }
) {
BleItem(
ble = it,
shapeType = filteredData.takeShapeType(it),
shapeType = state.bleList.takeShapeType(it),
onClick = {
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(
shapeType: ShapeType,
ble: BleInfo,
checked: Boolean,
onClick: () -> Unit
){
@ -313,7 +409,6 @@ fun BleItem(
val highAlpha = ContentAlpha.high
val disabledAlpha = ContentAlpha.disabled
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
@ -333,27 +428,118 @@ fun BleItem(
highAlpha
}
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
horizontal = 16.dp,
vertical = 12.dp
)
) {
BleItemContent(
ble = ble,
color = color,
time = time,
)
Checkbox(
checked = checked,
onCheckedChange = null
)
}
}
}
@Composable
fun BleItem(
shapeType: ShapeType,
ble: BleInfo,
onClick: () -> Unit
){
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.surfaceContainer
}
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
)
}
LaunchedEffect(ble.scanTime) {
while(true) {
time = SystemClock.elapsedRealtime()
delay(1000)
}
}
val alpha = if(time - ble.scanTime > 10_000){
ContentAlpha.disabled
} else {
ContentAlpha.high
}
Surface(
shape = shapeType.shape,
color = color,
onClick = onClick,
modifier = Modifier
.alpha(alpha)
.fillMaxWidth()
) {
BleItemContent(
ble = ble,
color = color,
time = time,
modifier = Modifier.padding(
horizontal = 16.dp, vertical = 12.dp
)
)
}
}
@Composable
private fun BleItemContent(
ble: BleInfo,
color: Color,
time: Long,
modifier: Modifier = Modifier
){
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clip(shapeType.shape)
.background(color)
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp)
.alpha(alpha)
modifier = modifier.fillMaxWidth()
) {
Box {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = ble.type.icon,
contentDescription = null
)
}
if(ble.tableStatus !== BleInfo.HistoryTableStatus.DISABLED){
@ -403,7 +589,7 @@ fun BleItem(
modifier = Modifier.alpha(0.7f)
) {
val color = if(ble.batteryLevel < 100){
val contentColor = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
@ -413,7 +599,7 @@ fun BleItem(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null,
tint = color
tint = contentColor
)
Box {
@ -427,7 +613,7 @@ fun BleItem(
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.batteryLevel.toString() + " %",
color = color
color = contentColor
)
}
@ -447,16 +633,8 @@ fun BleItem(
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) {
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(

View File

@ -1,17 +1,18 @@
package llc.arma.ble.app.ui.screen.ble
import android.os.SystemClock
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.GetBleAroundFlow
import llc.arma.ble.domain.usecase.filter.GetFilterFlow
import javax.inject.Inject
import kotlin.math.pow
@HiltViewModel
class BleListViewModel @Inject constructor(
@ -19,25 +20,8 @@ class BleListViewModel @Inject constructor(
private val getBleAroundFlow: GetBleAroundFlow
) : 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 =
BleListContract.State(emptyList(), BleFilter())
BleListContract.State(emptyList(), false, BleListContract.State.BleSummary(0,0,0,0))
override fun handleEvents(event: BleListContract.Event) {
when(event){
@ -86,6 +70,8 @@ class BleListViewModel @Inject constructor(
}
}
private var scannerJob: Job? = null
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnResetScanner
@ -93,12 +79,50 @@ class BleListViewModel @Inject constructor(
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 {
copy(
bleList = it
BleListContract.State(
bleList,
filter == BleFilter(),
BleListContract.State.BleSummary(
all = ble.size,
active = ble.filter { it.batteryLevel == 100 }
.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size,
lost = ble.count { SystemClock.elapsedRealtime() - it.scanTime > 10_000 },
lowBattery = ble.count { it.batteryLevel < 100 }
)
)
}
}.launchIn(viewModelScope)
}

View File

@ -1,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 class Display(
val origin: BleFilter,
val filter: BleFilter
) : State()

View File

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

View File

@ -22,7 +22,7 @@ class BleFilterViewModel @Inject constructor(
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,
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(

View File

@ -23,7 +23,10 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
@ -335,8 +338,6 @@ private fun DisplayState(
labelRotationDegrees = -90f,
)
val scrollState = rememberChartScrollState()
when(lastMeasure){
is Ble.Accelerometer.HistoryPoint.Acceleration,
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.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerContract {
sealed class Event : ViewEvent {
data object OnShowChart : Event()
data object OnNavigateUp : Event()
data object OnRestart : Event()
data object OnShowAccelerometerHistory : Event()
data object OnShowRealtimeForm : Event()
data object OnPowerEdit : Event()
data object OnShowWriteBlePreview : Event()
data object OnWriteBle : Event()
data object OnChangePassword : Event()
data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data object OnReadIntervalEdit : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
data object OnPowerEdit : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
val tx: Ble.BleState.TX
) : Event()
data object OnShowHistoryForm : Event()
@ -48,59 +50,30 @@ class AccelerometerContract {
val scale: AccelScale
) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer,
val writeState: WriteState?,
val accelViewMode: AccelViewMode,
val accelRealtimeViewMode: RealtimeViewMode,
val accelScale: AccelScale,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
val accelerometer: Ble.Accelerometer
) : State()
}
sealed class Effect : ViewSideEffect {
object ShowWriteBle : Effect()
sealed class Navigation : Effect() {
data class Write(
val serial: String,
val request: Ble.Accelerometer.WriteRequest
) : Effect()
data object ShowRealtimeForm : Effect()
data object ShowHistoryForm : Effect()
@ -114,7 +87,7 @@ class AccelerometerContract {
) : Navigation()
data class TxPowerSelector(
val tx: BleView.BleState.TX
val tx: Ble.BleState.TX
) : Navigation()
data class ChangePassword(
@ -122,14 +95,11 @@ class AccelerometerContract {
) : Navigation()
data class AccelHistory(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
val serial: String
) : 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.padding
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
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.AccelerometerHistoryFormDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeFormDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form.AccelerometerHistoryFormData
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -57,19 +44,14 @@ fun AccelerometerScreen(
navigator: DestinationsNavigator,
bleSerial: String,
historyFormResult: ResultRecipient<AccelerometerHistoryFormDestination, AccelerometerHistoryFormData>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
writeResult: ResultRecipient<AccelerometerWriteScreenDestination, Boolean>,
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
historyFormResult.onResult {
viewModel.setEvent(AccelerometerContract.Event.OnEnableSaveHistory(it.mode, it.scale))
}
@ -90,65 +72,16 @@ fun AccelerometerScreen(
}
LaunchedEffect(
key1 = bottomDialog.sheetState?.currentValue,
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
)
writeResult.onResult {
if(it) viewModel.setEvent(AccelerometerContract.Event.OnRestart)
}
}
}
null -> {
bottomDialog.hide()
}
}
}
DisposableEffect(Unit){
onDispose {
scope.launch {
bottomDialog.hide()
}
}
}
LaunchedEffect("effect"){
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
is AccelerometerContract.Effect.ShowWriteBle -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.Navigation.AccelHistory ->
navigator.navigate(AccelerometerHistoryDestination(it.ble.serial))
navigator.navigate(AccelerometerHistoryDestination(it.serial))
is AccelerometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.serial))
@ -156,13 +89,14 @@ fun AccelerometerScreen(
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "ReadIntervalSelector",
duration = it.interval
duration = it.interval,
minimum = 1000
))
is AccelerometerContract.Effect.Navigation.SaveIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "SaveIntervalSelector",
duration = it.interval
duration = it.interval,
))
is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
@ -173,6 +107,15 @@ fun AccelerometerScreen(
AccelerometerContract.Effect.Navigation.ShowRealtimeForm ->
navigator.navigate(AccelerometerRealtimeFormDestination(bleSerial))
is AccelerometerContract.Effect.Navigation.Write ->
navigator.navigate(AccelerometerWriteScreenDestination(
bleSerial = it.serial,
writeRequest = it.request
))
AccelerometerContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}.launchIn(this)
}
@ -194,6 +137,20 @@ fun AccelerometerScreen(
},
title = {
Text(text = BleInfo.Type.ACCELEROMETER.localized)
},
actions = {
if(state is AccelerometerContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(AccelerometerContract.Event.OnRestart)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
}
)
}
@ -205,13 +162,11 @@ fun AccelerometerScreen(
when(state){
is AccelerometerContract.State.Display -> {
DisplayState(
origin = state.origin,
ble = state.accelerometer,
onEvent = viewModel::setEvent
)
DisplayState(viewModel, state)
}
is AccelerometerContract.State.Loading -> LoadingState()
is AccelerometerContract.State.Loading -> LoadingState(
viewModel, state
)
}
}

View File

@ -4,18 +4,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.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.WriteBle
import javax.inject.Inject
@ -23,66 +17,21 @@ import javax.inject.Inject
@HiltViewModel
class AccelerometerViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
private val getBleBySerial: GetBleBySerial,
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
init {
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
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
)
}
}
}
}
loadData()
}
}
override fun setInitialState() = AccelerometerContract.State.Loading
override fun setInitialState() = AccelerometerContract.State.Loading(null)
override fun handleEvents(event: AccelerometerContract.Event) {
when(event){
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnDisableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
@ -93,22 +42,32 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnEnableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowHistoryForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowChart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowRealtimeForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnRestart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowChart
event: AccelerometerContract.Event.OnNavigateUp
) {
setEffect {
AccelerometerContract.Effect.Navigation.ShowHistoryForm
AccelerometerContract.Effect.Navigation.Up
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnRestart
) {
loadData()
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowRealtimeForm
@ -138,7 +97,15 @@ class AccelerometerViewModel @Inject constructor(
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) {
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) {
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) {
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,
mode = event.mode,
detailed = true
)
)
)
)
}
}
@ -246,12 +237,7 @@ class AccelerometerViewModel @Inject constructor(
setEffect {
AccelerometerContract.Effect.Navigation.AccelHistory(
ble = state.accelerometer.info,
accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
serial = state.accelerometer.info.serial
)
}
@ -261,12 +247,12 @@ class AccelerometerViewModel @Inject constructor(
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer
val newBle = state.accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest(
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,
)
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
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) {
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(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
private var loadJob: Job? = null
if(state is AccelerometerContract.State.Display){
private fun loadData(){
state.writeState?.let { request ->
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
loadJob?.cancel()
loadJob = viewModelScope.launch {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
AccelerometerContract.State.Loading(null)
}
writeBle(state.accelerometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistorySettings = request.writeRequest.saveHistorySettings
?: currentState.origin.accelerometerState.saveHistorySettings
)
)
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
AccelerometerContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
}
},
onFailure = {
if( ble is Ble.Accelerometer){
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
when(this){
is AccelerometerContract.State.Display -> {
copy(
origin = Ble.Accelerometer(
info = ble.info,
state = origin.state,
accelerometerState = origin.accelerometerState
)
)
}
is AccelerometerContract.State.Loading -> {
AccelerometerContract.State.Display(
origin = ble,
accelerometer = ble
)
}
}
)
}
}
}
}

View File

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

View File

@ -1,28 +1,31 @@
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
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.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerViewModel
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.app.ui.screen.locale.value
import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.Ble
import kotlin.time.DurationUnit
@ -30,9 +33,8 @@ import kotlin.time.toDuration
@Composable
fun DisplayState(
onEvent: (AccelerometerContract.Event) -> Unit,
origin: Ble.Accelerometer,
ble: BleView.Accelerometer
viewModel: AccelerometerViewModel,
state: AccelerometerContract.State.Display
) {
val scrollState = rememberScrollState()
@ -48,8 +50,8 @@ fun DisplayState(
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
@ -59,7 +61,7 @@ fun DisplayState(
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
subtitle = "${state.accelerometer.state.tx.value} db",
icon = {
Icon(
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(
shapeType = ShapeType.Middle,
@ -82,12 +84,12 @@ fun DisplayState(
},
icon = {
Switch(
checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled,
checked = state.accelerometer.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled,
onCheckedChange = {
if(it){
onEvent(AccelerometerContract.Event.OnShowHistoryForm)
viewModel.setEvent(AccelerometerContract.Event.OnShowHistoryForm)
} 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(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = ble.accelerometerState.historyInterval
subtitle = state.accelerometer.accelerometerState.historyInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
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(
shapeType = ShapeType.Middle,
title = "Интервал чтения",
subtitle = ble.accelerometerState.readInterval
subtitle = state.accelerometer.accelerometerState.readInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
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 ->
onEvent(AccelerometerContract.Event.OnShowRealtimeForm)
viewModel.setEvent(AccelerometerContract.Event.OnShowRealtimeForm)
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(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
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.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LoadingState(){
fun LoadingState(
viewModel: AccelerometerViewModel,
state: AccelerometerContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
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.ViewState
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
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object OnRefresh : Event()
}
sealed class State : ViewState {
data class Display(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint>
data class Loading(
val attempt: Int?
) : 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 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.launch
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.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import javax.inject.Inject
@ -26,17 +26,15 @@ class AccelerometerAccelViewModel @Inject constructor(
private var measureJob: Job? = null
init {
startReadMeasure(false)
startReadMeasure()
}
override fun setInitialState() = AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
override fun setInitialState() = AccelerometerAccelContract.State.Loading(null)
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
@ -44,26 +42,36 @@ class AccelerometerAccelViewModel @Inject constructor(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefresh
) {
startReadMeasure(true)
startReadMeasure()
}
private fun startReadMeasure(
restartJob: Boolean
){
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnNavigateUp
) {
setEffect { AccelerometerAccelContract.Effect.Navigation.Up }
}
private fun startReadMeasure() {
val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle)
if(restartJob || measureJob == null) {
setState {
AccelerometerAccelContract.State.Loading(null)
}
measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch {
val flow = retryUntilNotNull(
onNewAttempt = {
setState {
AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
AccelerometerAccelContract.State.Loading(it)
}
}
) {
getAccelerometerMeasureBySerialFlow(
params.bleSerial,
@ -71,44 +79,73 @@ class AccelerometerAccelViewModel @Inject constructor(
params.accelMode,
params.fftAxis,
params.fftMode,
params.frequency
).onEach {
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
)
FftFrequency.F_400
).getOrNull()
}
AccelerometerAccelContract.State.Exception -> {
AccelerometerAccelContract.State.Display(
flow.onEach {
val state = viewState.value
val newState = when (it) {
is Ble.Accelerometer.RealtimePoint.Angle -> {
if (state is AccelerometerAccelContract.State.DisplayAngle) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayAngle(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Common -> {
if (state is AccelerometerAccelContract.State.DisplayCommon) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayCommon(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Rotation -> {
if (state is AccelerometerAccelContract.State.DisplayRotation) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayRotation(
params.accelMode,
listOf(it)
)
}
}
is Ble.Accelerometer.RealtimePoint.Vibration -> {
if (state is AccelerometerAccelContract.State.DisplayVibration) {
state.copy(
measureHistory = (state.measureHistory + it).takeLast(100)
)
} else {
AccelerometerAccelContract.State.DisplayVibration(
params.accelMode,
listOf(it)
)
}
}
}
},
onFailure = {
setState {
AccelerometerAccelContract.State.Exception
}
}
)
setState { newState }
}.launchIn(this)
}
}
}
}

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import android.graphics.Color
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
@ -26,7 +28,9 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.component.shape.ShapeComponent
import com.patrykandpatrick.vico.core.component.text.TextComponent
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
@ -51,9 +57,10 @@ import com.patrykandpatrick.vico.core.scroll.InitialScroll
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
@ -76,6 +83,15 @@ fun AccelerometerRealtime(
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when (it) {
AccelerometerAccelContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}
}
Scaffold(
topBar = {
TopAppBar(
@ -96,6 +112,9 @@ fun AccelerometerRealtime(
)
},
actions = {
if((state is AccelerometerAccelContract.State.Loading).not()) {
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
@ -109,14 +128,21 @@ fun AccelerometerRealtime(
}
}
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
Box(
modifier = Modifier.padding(it)
) {
when (state) {
is AccelerometerAccelContract.State.Display -> DisplayState(state = state)
is AccelerometerAccelContract.State.Exception -> ExceptionState()
is AccelerometerAccelContract.State.DisplayAngle -> DisplayAngleState(state)
is AccelerometerAccelContract.State.DisplayCommon -> DisplayCommonState(state)
is AccelerometerAccelContract.State.DisplayRotation -> DisplayRotationState(state)
is AccelerometerAccelContract.State.DisplayVibration -> DisplayVibrationState(state)
is AccelerometerAccelContract.State.Loading -> LoadingState(viewModel, state)
}
}
@ -124,10 +150,188 @@ fun AccelerometerRealtime(
}
@Composable
private fun DisplayCommonState(
state: AccelerometerAccelContract.State.DisplayCommon
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.fillMaxSize()
) {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.x)
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.y)
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint.z)
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
lineComponent = ShapeComponent(color = Color.TRANSPARENT),
thresholdValue = 0f
)
),
persistentMarkers = mapOf(
xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Surface(
shape = ShapeType.Start.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(top = 8.dp)
) {
Text(
text = "Ось X",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.Middle.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = "Ось Y",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
Surface(
shape = ShapeType.End.shape,
color = MaterialTheme.colorScheme.surfaceContainer,
modifier = Modifier.weight(1f)
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.padding(bottom = 8.dp)
) {
Text(
text = "Ось Z",
style = MaterialTheme.typography.titleSmall
)
Chart(
marker = marker,
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
@Composable
private fun DisplayState(
state: AccelerometerAccelContract.State.Display
private fun DisplayAngleState(
state: AccelerometerAccelContract.State.DisplayAngle
) {
Box(modifier = Modifier
@ -159,68 +363,20 @@ private fun DisplayState(
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.x )
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.angle )
}
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.y )
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.y)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.tmp)
}
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.RealtimePoint.Common ->
FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Vibration ->
FloatEntry(index.toFloat(), measurePoint.value)
is Ble.Accelerometer.RealtimePoint.Angle ->
FloatEntry(index.toFloat(), measurePoint.z)
is Ble.Accelerometer.RealtimePoint.Rotation ->
FloatEntry(index.toFloat(), measurePoint.turnovers.toFloat())
}
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
),
persistentMarkers = mapOf(xProducer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
val lastMeasure = state.measureHistory.last()
val lastMeasure = state.measureHistory.lastOrNull()
when(lastMeasure){
is Ble.Accelerometer.RealtimePoint.Angle -> {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
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(
verticalArrangement = Arrangement.spacedBy(16.dp),
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(
marker = marker,
@ -349,9 +618,7 @@ private fun DisplayState(
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
modifier = Modifier.fillMaxSize(),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
@ -361,77 +628,9 @@ 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
@ -504,21 +703,21 @@ fun Angle(
}
@Composable
private fun ExceptionState(
private fun LoadingState(
viewModel: AccelerometerAccelViewModel,
state: AccelerometerAccelContract.State.Loading
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
.fillMaxSize(),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
RetryingLoadingTemplate(
attempt = state.attempt
) {
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.Column
@ -9,15 +9,12 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
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.AccelerometerSpectreDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.parcelize.Parcelize
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.locale.localized
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
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.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
@ -25,7 +21,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
@ -50,7 +49,6 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
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.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract.Effect.Navigation
import llc.arma.ble.domain.model.Ble
@ -13,11 +12,7 @@ class BeaconContract {
data object OnNavigateUp : Event()
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
object OnShowWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event()
data object OnPowerEdit : Event()
@ -25,56 +20,36 @@ class BeaconContract {
val ble: Ble.Beacon
) : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
data class OnTxChanged(
val tx: Ble.BleState.TX
) : Event()
data class OnTxChanged(val tx: BleView.BleState.TX) : Event()
object OnNavigateUpClicked : Event()
object OnChangePassword : Event()
data object OnChangePassword : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Beacon,
val beacon: BleView.Beacon,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
val beacon: Ble.Beacon
) : State()
}
sealed class Effect : ViewSideEffect {
data object HideWriteBlePreview : Effect()
data object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() {
data class Write(
val bleSerial: String,
val writeRequest: Ble.Beacon.WriteRequest
) : Navigation()
data object Up : Navigation()
data class PasswordForm(
@ -82,7 +57,7 @@ class BeaconContract {
) : Navigation()
data class TxSelector(
val tx: BleView.BleState.TX?
val tx: Ble.BleState.TX?
) : Navigation()
}

View File

@ -1,12 +1,10 @@
package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@ -17,53 +15,39 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.BeaconWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BeaconScreen(
bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BeaconViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult {
viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
}
@ -72,15 +56,6 @@ fun BeaconScreen(
viewModel.effect.onEach {
when(it){
BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
BeaconContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is BeaconContract.Effect.Navigation.PasswordForm ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
@ -88,35 +63,14 @@ fun BeaconScreen(
navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
BeaconContract.Effect.Navigation.Up ->
navigator.popBackStack()
navigator.navigateUp()
is BeaconContract.Effect.Navigation.Write ->
navigator.navigate(BeaconWriteScreenDestination(it.bleSerial, it.writeRequest))
}
}.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(
topBar = {
TopAppBar(
@ -144,34 +98,30 @@ fun BeaconScreen(
) {
when(state){
is BeaconContract.State.Display -> DisplayState(
onEvent = {
viewModel.setEvent(it)
},
ble = state.beacon,
origin = state.origin
)
is BeaconContract.State.Loading -> LoadingState()
is BeaconContract.State.Display -> DisplayState(viewModel, state)
is BeaconContract.State.Loading -> LoadingState(viewModel, state)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
private fun LoadingState(
viewModel: BeaconViewModel,
state: BeaconContract.State.Loading,
){
Box(
contentAlignment = Alignment.Center,
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.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.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.WriteBle
import javax.inject.Inject
@ -26,9 +18,6 @@ import javax.inject.Inject
class BeaconViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
init {
@ -37,12 +26,17 @@ class BeaconViewModel @Inject constructor(
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
BeaconContract.State.Loading(it)
}
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if(ble != null && ble is Ble.Beacon){
if(ble is Ble.Beacon){
setState {
when(this){
@ -54,11 +48,10 @@ class BeaconViewModel @Inject constructor(
)
)
}
BeaconContract.State.Loading -> {
is BeaconContract.State.Loading -> {
BeaconContract.State.Display(
origin = ble,
beacon = bleMapper.map(ble) as BleView.Beacon,
writeState = null
beacon = ble
)
}
}
@ -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) {
when(event){
is BeaconContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is BeaconContract.Event.OnTxChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnBleChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnChangePassword -> reduce(viewState.value, event)
is BeaconContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is BeaconContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is BeaconContract.Event.OnWriteBle -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is BeaconContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnPowerChanged
) {
if(state is BeaconContract.State.Display) {
state.beacon.state.tx = event.tx
}
}
private fun reduce(
state: BeaconContract.State,
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(
state: BeaconContract.State,
event: BeaconContract.Event.OnTxChanged
) {
if(state is BeaconContract.State.Display){
setState {
state.copy(
beacon = state.beacon.copy(
state = state.beacon.state.copy(
tx = event.tx
)
)
)
}
}
}
private fun reduce(
@ -159,8 +137,7 @@ class BeaconViewModel @Inject constructor(
setState {
BeaconContract.State.Display(
origin = event.ble,
beacon = bleMapper.map(event.ble) as BleView.Beacon,
writeState = null
beacon = event.ble
)
}
}
@ -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(
state: BeaconContract.State,
event: BeaconContract.Event.OnShowWriteBlePreview
@ -197,83 +165,16 @@ class BeaconViewModel @Inject constructor(
if(state is BeaconContract.State.Display){
val newBle = bleViewMapper.map(state.beacon) as Ble.Beacon
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
val newBle = state.beacon
val writeRequest = Ble.Beacon.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx
)
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
BeaconContract.Effect.ShowWriteBlePreview
}
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnWriteBle
) {
if(state is BeaconContract.State.Display){
state.writeState?.let { request ->
if(request is BeaconContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is BeaconContract.State.Display) {
val newBleObject = Ble.Beacon(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
)
)
writeBle(state.beacon.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
beacon = bleMapper.map(newBleObject) as BleView.Beacon,
writeState = BeaconContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = BeaconContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
BeaconContract.Effect.Navigation.Write(params.bleSerial, writeRequest)
}
}

View File

@ -1,49 +1,49 @@
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
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.ShapeType
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.domain.model.Ble
import llc.arma.ble.app.ui.screen.locale.value
@Composable
fun DisplayState(
onEvent: (BeaconContract.Event) -> Unit,
origin: Ble.Beacon,
ble: BleView.Beacon
viewModel: BeaconViewModel,
state: BeaconContract.State.Display
) {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
@ -53,7 +53,7 @@ fun DisplayState(
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
subtitle = "${state.beacon.state.tx.value} db",
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
@ -61,7 +61,7 @@ fun DisplayState(
)
}
) {
onEvent(BeaconContract.Event.OnPowerEdit)
viewModel.setEvent(BeaconContract.Event.OnPowerEdit)
}
BleMenuItem(
@ -75,16 +75,27 @@ fun DisplayState(
}
) {
onEvent(BeaconContract.Event.OnChangePassword)
viewModel.setEvent(BeaconContract.Event.OnChangePassword)
}
}
Button (
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.beacon) {
Button(
onClick = {
onEvent(BeaconContract.Event.OnShowWriteBlePreview)
viewModel.setEvent(BeaconContract.Event.OnShowWriteBlePreview)
},
modifier = Modifier.fillMaxWidth()
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.height(48.dp)
) {
Text(
@ -95,4 +106,8 @@ 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.TopAppBar
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -244,7 +249,7 @@ private fun LoadingState(
if(state.progress == null){
ContainedLoadingIndicator()
}else{
} else {
ContainedLoadingIndicator(
progress = { state.progress }
)

View File

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

View File

@ -9,24 +9,19 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.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
@ -38,25 +33,20 @@ import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScr
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@ -64,19 +54,18 @@ enum class SheetPage {
fun GateScreen(
bleSerial: String,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
writeResult: ResultRecipient<GateWriteScreenDestination, Boolean>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<GateViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
writeResult.onResult {
if(it) viewModel.setEvent(GateContract.Event.OnReload)
}
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult {
viewModel.setEvent(GateContract.Event.OnPowerChanged(it))
}
@ -96,11 +85,6 @@ fun GateScreen(
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
GateContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is GateContract.Effect.Navigation.BleTable ->
navigator.navigate(GateBleTableScreenDestination(it.serial))
@ -131,35 +115,13 @@ fun GateScreen(
maximum = 10 * 24 * 60 * 60 * 1000
))
is GateContract.Effect.Navigation.GateWrite ->
navigator.navigate(GateWriteScreenDestination(it.serial, it.request))
}
}.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(
topBar = {
TopAppBar(
@ -179,6 +141,20 @@ fun GateScreen(
Text(
text = BleInfo.Type.HOST.localized
)
},
actions = {
if(state is GateContract.State.Display){
IconButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnReload)
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
}
)
}
@ -199,11 +175,10 @@ fun GateScreen(
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(
viewModel: GateViewModel,
state: GateContract.State.Loading
state: GateContract.State.Loading,
){
Box(
@ -211,44 +186,9 @@ private fun LoadingState(
modifier = Modifier.fillMaxSize()
){
Column(
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 = {
RetryingLoadingTemplate(state.attempt){
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
@ -20,68 +19,12 @@ import javax.inject.Inject
class GateViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<GateContract.State, GateContract.Event, GateContract.Effect>() {
init {
val params = GateScreenDestination.argsFrom(savedStateHandle)
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
)
}
}
}
}
}
loadData()
}
@ -91,8 +34,6 @@ class GateViewModel @Inject constructor(
when(event){
is GateContract.Event.OnNavigateUp -> 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.OnPowerChanged -> 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.OnSaveReadIntervalChanged -> 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) {
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) {
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) {
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) {
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) {
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) {
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(
state: GateContract.State,
event: GateContract.Event.OnHideWriteBlePreview
) {
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowWriteBlePreview
event: GateContract.Event.OnWriteBle
) {
if(state is GateContract.State.Display){
val newBle = bleViewMapper.map(state.gate) as Ble.Gate
val newBle = state.gate
val writeRequest = Ble.Gate.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
@ -274,7 +238,7 @@ class GateViewModel @Inject constructor(
}
setEffect {
GateContract.Effect.ShowWriteBlePreview
GateContract.Effect.Navigation.GateWrite(state.gate.info.serial, writeRequest)
}
}
@ -283,63 +247,61 @@ class GateViewModel @Inject constructor(
private fun reduce(
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 {
state.copy(
writeState = GateContract.State.Display.WriteState.Writing(request.writeRequest)
)
GateContract.State.Loading(null)
}
val currentState = viewState.value
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 = {
val ble = retryUntilNotNull(
onNewAttempt = {
setState {
currentState.copy(
origin = newBleObject,
gate = bleMapper.map(newBleObject) as BleView.Gate,
writeState = GateContract.State.Display.WriteState.Success
)
GateContract.State.Loading(it)
}
},
onFailure = {
}
){
getBleBySerial.invoke(params.bleSerial, this).getOrNull()
}
if (ble is Ble.Gate) {
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
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -16,16 +18,12 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
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.domain.model.Ble
import llc.arma.ble.app.ui.screen.locale.value
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@ -37,9 +35,12 @@ fun DisplayState(
val scrollState = rememberScrollState()
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.weight(1f)
.verticalScroll(scrollState)
.padding(horizontal = 16.dp)
) {
@ -70,10 +71,11 @@ fun DisplayState(
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = state.gate.hostState.historyInterval
subtitle = state.gate.gateState.historyInterval
.toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
"$hours ч. $minutes мин. $seconds сек."
},
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
@ -87,10 +89,11 @@ fun DisplayState(
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал чтения",
subtitle = state.gate.hostState.readInterval
subtitle = state.gate.gateState.readInterval
.toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
"$hours ч. $minutes мин. $seconds сек."
},
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
@ -142,15 +145,32 @@ fun DisplayState(
}
}
Box(
modifier = Modifier.fillMaxWidth().animateContentSize()
) {
if(state.origin != state.gate) {
Button(
onClick = {
viewModel.setEvent(GateContract.Event.OnShowWriteBlePreview)
viewModel.setEvent(GateContract.Event.OnWriteBle)
},
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.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 {
data object OnHideWritePreview: Event()
data object OnWritePreview: Event()
data object OnWrite: Event()
data object OnWriteTable: Event()
data object OnRestart : Event()
data object OnSelectBle : Event()
data class OnBleSelected(
val bleSerials: List<BleName>
) : Event()
data class OnAddBle(
val ble: BleName
) : Event()
@ -26,34 +28,14 @@ class GateBleTableContract {
sealed class State : ViewState {
data object Loading : State()
data object Error : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val bleAround: List<BleInfo>,
val newTable: List<BleName>,
val savedBleTable: List<BleName>,
val writeState: WriteState?
) : 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()
}
}
) : State()
}
@ -61,6 +43,14 @@ class GateBleTableContract {
sealed class Navigation : Effect() {
data class WriteTable(
val table: List<BleName>
) : Navigation()
data class BleSelector(
val selected: List<BleName>
) : Navigation()
data object Up : Navigation()
}

View File

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

View File

@ -2,13 +2,14 @@ package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.domain.model.BleName
import llc.arma.ble.domain.usecase.AddBleToHostTable
import llc.arma.ble.domain.usecase.GetBleNamesFlow
@ -18,10 +19,8 @@ import javax.inject.Inject
@HiltViewModel
class GateBleTableViewModel @Inject constructor(
private val getFoundBle: GetFoundBle,
private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow,
private val addBleToHostTable: AddBleToHostTable,
private val getHostBleTableBySerial: GetHostBleTableBySerial
) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() {
@ -29,110 +28,66 @@ class GateBleTableViewModel @Inject constructor(
setEvent(GateBleTableContract.Event.OnRestart)
viewModelScope.launch {
while (true){
val state = viewState.value
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(bleAround = getFoundBle())
}
}
delay(1_000)
}
}
}
override fun setInitialState() = GateBleTableContract.State.Loading
override fun setInitialState() = GateBleTableContract.State.Loading(null)
override fun handleEvents(event: GateBleTableContract.Event) {
when(event){
is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWritePreview -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWrite -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWriteTable -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnSelectBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnBleSelected -> reduce(viewState.value, event)
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWrite
) {
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
event: GateBleTableContract.Event.OnSelectBle
) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(writeState = null)
}
}
}
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(
setEffect {
GateBleTableContract.Effect.Navigation.BleSelector(
state.newTable
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnBleSelected
) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(
newTable = event.bleSerials
)
}
}
}
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWriteTable
) {
if(state is GateBleTableContract.State.Display) {
setEffect {
GateBleTableContract.Effect.Navigation.WriteTable(
state.newTable
)
}
@ -150,7 +105,7 @@ class GateBleTableViewModel @Inject constructor(
if(state.newTable.any { it.serial == event.ble.serial}){
setState {
state.copy(newTable = state.newTable.filter { it.serial != event.ble.serial})
state.copy(newTable = state.newTable.filter { it.serial != event.ble.serial })
}
} else {
@ -165,6 +120,8 @@ class GateBleTableViewModel @Inject constructor(
}
private var loadJob: Job? = null
private fun reduce(
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnRestart
@ -174,37 +131,35 @@ class GateBleTableViewModel @Inject constructor(
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState {
GateBleTableContract.State.Loading
GateBleTableContract.State.Loading(null)
}
viewModelScope.launch {
loadJob?.cancel()
loadJob = viewModelScope.launch {
val names = getBleNamesFlow.invoke().first()
getHostBleTableBySerial(params.bleSerial).fold(
onSuccess = {
val table = retryUntilNotNull(
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 ?: "Безымянный",
serial = ble) }
setState {
GateBleTableContract.State.Display(
bleAround = emptyList(),
newTable = savedBle,
savedBleTable = savedBle,
writeState = null
savedBleTable = savedBle
)
}
},
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.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -40,11 +34,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
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
data class DurationSelectResult(
@ -58,7 +48,7 @@ fun DurationSelectorScreen(
qualifier: String? = null,
duration: Int?,
minimum: Int = 10_000,
maximum: Int = 10 * 24 * 60 * 60 * 1000,
maximum: Int = 864_000_000,
daysComponent: Boolean = true,
hoursComponent: Boolean = true,
minutesComponent: Boolean = true,
@ -82,17 +72,16 @@ fun DurationSelectorScreen(
}
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth()
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier.padding(20.dp).fillMaxWidth()
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
modifier = Modifier,
text = "Интервал измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
DurationPicker(
minInterval = minimum,
maxInterval = maximum,
@ -106,13 +95,25 @@ fun DurationSelectorScreen(
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(
onClick = {
viewModel.setEvent(DurationSelectorContract.Event.OnSave)
},
modifier = Modifier.fillMaxWidth()
}
) {
Text(
text = "Применить"
@ -122,12 +123,14 @@ fun DurationSelectorScreen(
}
}
}
@Composable
fun DurationPicker(
modifier: Modifier,
maxInterval: Int = 10 * 24 * 60 * 60 * 1000,
maxInterval: Int = 864_000_000,
minInterval: Int = 10_000,
seconds: Boolean = true,
minutes: Boolean = true,
@ -137,6 +140,7 @@ fun DurationPicker(
onChanged: (duration: Int) -> Unit
){
LaunchedEffect(value, maxInterval, minInterval) {
if(value > maxInterval){
onChanged(maxInterval)
}
@ -144,6 +148,7 @@ fun DurationPicker(
if(value < minInterval){
onChanged(minInterval)
}
}
val maxSeconds = maxInterval / millisInSecond
val maxMinutes = maxInterval / millisInMinute
@ -155,13 +160,20 @@ fun DurationPicker(
val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute
val secondsValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour) - (minutesValue * millisInMinute)) / millisInSecond
println("${maxInterval} ${minInterval} ${maxDays} ${maxHours} ${maxMinutes} ${maxSeconds}")
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
if(days) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker(
range = -1..maxDays,
value = dayValue,
@ -170,15 +182,19 @@ fun DurationPicker(
}
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "Д.")
}
}
if(hours) {
Spacer(modifier = Modifier.width(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker(
range = -1..maxHours,
@ -188,15 +204,19 @@ fun DurationPicker(
}
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "Ч.")
}
}
if(minutes) {
Spacer(modifier = Modifier.width(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker(
range = -1..maxMinutes,
@ -206,15 +226,19 @@ fun DurationPicker(
}
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "М.")
}
}
if(seconds) {
Spacer(modifier = Modifier.width(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
NumberPicker(
range = -1..maxSeconds,
@ -224,7 +248,7 @@ fun DurationPicker(
}
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(4.dp))
Text(text = "С.")
@ -232,6 +256,8 @@ fun DurationPicker(
}
}
}
const val millisInSecond = 1000
@ -250,6 +276,7 @@ fun NumberPicker(
LaunchedEffect(range){
if(value > range.last){
onValueChanged(range.last)
@ -264,12 +291,14 @@ fun NumberPicker(
}
println(value)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
){
FilledIconButton(
FilledTonalIconButton(
onClick = {
if(value < range.last) onValueChanged(value + 1)
}
@ -304,7 +333,7 @@ fun NumberPicker(
Spacer(modifier = Modifier.height(36.dp))
FilledIconButton(
FilledTonalIconButton(
onClick = {
if(value > range.first) onValueChanged(value - 1)

View File

@ -1,7 +1,6 @@
package llc.arma.ble.app.ui.screen.inspection.selector.duration
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
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.ViewSideEffect
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 {
@ -12,7 +12,7 @@ class TxPowerSelectorContract {
data object OnNavigateUp : Event()
data class OnSelected(
val tx: BleView.BleState.TX
val tx: Ble.BleState.TX
) : Event()
data object OnSave : Event()
@ -20,7 +20,7 @@ class TxPowerSelectorContract {
}
data class State(
val tx: BleView.BleState.TX?
val tx: Ble.BleState.TX?
) : ViewState
sealed class Effect : ViewSideEffect {
@ -30,7 +30,7 @@ class TxPowerSelectorContract {
data object NavigateUp : Navigation()
data class NavigateUpWithResult(
val tx: BleView.BleState.TX
val tx: Ble.BleState.TX
) : Navigation()
}

View File

@ -1,37 +1,41 @@
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.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
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.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
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 llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.SelectorItem
import com.ramcosta.composedestinations.spec.DestinationStyle
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.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
fun TxPowerSelectorScreen(
tx: BleView.BleState.TX?,
resultNavigator: ResultBackNavigator<BleView.BleState.TX>
tx: Ble.BleState.TX?,
resultNavigator: ResultBackNavigator<Ble.BleState.TX>
) {
val viewModel = hiltViewModel<TxPowerSelectorViewModel>()
@ -48,32 +52,101 @@ fun TxPowerSelectorScreen(
}
}
Column {
Surface(
shape = RoundedCornerShape(20.dp),
modifier = Modifier.fillMaxWidth(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(20.dp)
) {
Text(
text = "Мощность",
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(
shapeType = Ble.BleState.TX.entries.takeShapeType(it),
label = "${it.value} dBb (${it.powerPercentage} %)",
selected = it == state.tx
){
) {
viewModel.setEvent(TxPowerSelectorContract.Event.OnSelected(it))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
PrimaryButton(
label = "Применить"
Row(
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)
}
) {
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
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import llc.arma.ble.app.ui.common.BaseViewModel

View File

@ -18,8 +18,13 @@ class ThermometerHistoryContract {
sealed class State : ViewState {
data class Loading(
val attempt: Int?,
val progress: Float?
) : State()
data class Display(
val loadingHistoryState : ProgressState<List<Ble.Thermometer.HistoryPoint>>
val history : List<Ble.Thermometer.HistoryPoint>
) : 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.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntry
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.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.domain.common.ProgressState
import java.text.SimpleDateFormat
import java.util.Date
@ -96,13 +102,9 @@ fun ThermometerHistoryScreen(
title = {
val title = when(state){
is ThermometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "История (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "История"
is ProgressState.Progress -> "История"
"История (${state.history.size})"
}
}
ThermometerHistoryContract.State.Exception -> "История"
else -> "История"
}
Text(
@ -116,10 +118,7 @@ fun ThermometerHistoryScreen(
onClick = {
viewModel.setEvent(ThermometerHistoryContract.Event.OnRefresh)
},
enabled = when(state){
is ThermometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
ThermometerHistoryContract.State.Exception -> true
}
enabled = state is ThermometerHistoryContract.State.Display
) {
Icon(
imageVector = Icons.Rounded.Refresh,
@ -138,6 +137,7 @@ fun ThermometerHistoryScreen(
when (state) {
is ThermometerHistoryContract.State.Display -> DisplayState(state = state)
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
private fun DisplayState(
state: ThermometerHistoryContract.State.Display
) {
Box(modifier = Modifier
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
if(state.history.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
@ -171,8 +201,8 @@ private fun DisplayState(
} else {
val producer = remember(state.loadingHistoryState.data) {
state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
val producer = remember(state.history) {
state.history.mapIndexed { index, measurePoint ->
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}.let {
ChartEntryModelProducer(it)
@ -194,9 +224,11 @@ private fun DisplayState(
Chart(
chartScrollState = scrollState,
chartScrollSpec = rememberChartScrollSpec(initialScroll = InitialScroll.End),
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
runInitialAnimation = false,
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
@ -205,38 +237,6 @@ private fun DisplayState(
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.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
import javax.inject.Inject
@ -24,8 +24,8 @@ class ThermometerHistoryViewModel @Inject constructor(
}
override fun setInitialState() = ThermometerHistoryContract.State.Display(
ProgressState.Indeterminate
override fun setInitialState() = ThermometerHistoryContract.State.Loading(
null, null
)
override fun handleEvents(event: ThermometerHistoryContract.Event) {
@ -60,22 +60,21 @@ class ThermometerHistoryViewModel @Inject constructor(
val params = ThermometerHistoryScreenDestination.argsFrom(savedStateHandle)
setState {
ThermometerHistoryContract.State.Display(ProgressState.Indeterminate)
ThermometerHistoryContract.State.Loading(null, null)
}
getTemperatureHistoryBySerial(params.bleSerial).collect {
it.fold(
onSuccess = {
val history = retryUntilNotNull(
onNewAttempt = {
setState {
ThermometerHistoryContract.State.Display(it)
ThermometerHistoryContract.State.Loading(it, null)
}
},
onFailure = {
}
){
getTemperatureHistoryBySerial(params.bleSerial).getOrNull()
}
setState {
ThermometerHistoryContract.State.Exception
}
}
)
ThermometerHistoryContract.State.Display(history)
}
}

View File

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

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.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import llc.arma.ble.app.ui.common.RetryingLoadingTemplate
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LoadingState(){
fun LoadingState(
viewModel: ThermometerViewModel,
state: ThermometerContract.State.Loading,
){
Box(
contentAlignment = Alignment.Center,
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.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
class ThermometerContract {
sealed class Event : ViewEvent {
data object OnRestart : Event()
data object OnTxSelect : Event()
data object OnWriteBle : Event()
data object OnHideWriteBlePreview : Event()
data object OnShowWriteBlePreview : Event()
data object OnShowTemperatureHistory : Event()
@ -27,7 +24,7 @@ class ThermometerContract {
) : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
val tx: Ble.BleState.TX
) : Event()
data object OnSaveIntervalEdit : Event()
@ -42,42 +39,26 @@ class ThermometerContract {
sealed class State : ViewState {
data object Loading : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Thermometer,
val thermometer: BleView.Thermometer,
val writeState: WriteState?
) : 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()
}
}
val thermometer: Ble.Thermometer
) : State()
}
sealed class Effect : ViewSideEffect {
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
sealed class Navigation : Effect() {
data class Write(
val bleSerial: String,
val writeRequest: Ble.Thermometer.WriteRequest
) : Navigation()
data object Up : Navigation()
data class DurationSelector(
@ -85,7 +66,7 @@ class ThermometerContract {
) : Navigation()
data class TxSelector(
val tx: BleView.BleState.TX?
val tx: Ble.BleState.TX?
) : Navigation()
data class ChangePassword(

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -23,54 +24,32 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerWriteScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import kotlin.time.DurationUnit
import kotlin.time.toDuration
enum class SheetPage {
WRITE
}
val Boolean.localizedName: String
get() {
return if(this){
"Включено"
} else {
"Выключено"
}
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThermometerScreen(
bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, Ble.BleState.TX>,
durationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
writeResult: ResultRecipient<ThermometerWriteScreenDestination, Boolean>,
navigator: DestinationsNavigator
) {
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val viewModel = hiltViewModel<ThermometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult {
viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
}
@ -79,44 +58,13 @@ fun ThermometerScreen(
viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
}
LaunchedEffect(sheetPage){
when(sheetPage){
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()
}
}
writeResult.onResult {
if(it) viewModel.setEvent(ThermometerContract.Event.OnRestart)
}
LaunchedEffect(Unit){
viewModel.effect.onEach {
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 ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
@ -136,8 +84,12 @@ fun ThermometerScreen(
))
ThermometerContract.Effect.Navigation.Up ->
navigator.popBackStack()
navigator.navigateUp()
is ThermometerContract.Effect.Navigation.Write ->
navigator.navigate(ThermometerWriteScreenDestination(
it.bleSerial, it.writeRequest
))
}
}.launchIn(this)
@ -161,6 +113,20 @@ fun ThermometerScreen(
},
title = {
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){
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.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
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.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.common.retryUntilNotNull
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class ThermometerViewModel @Inject constructor(
private val getBleBySerial: GetBleBySerial,
private val savedStateHandle: SavedStateHandle,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() {
init {
val params = ThermometerScreenDestination.argsFrom(savedStateHandle)
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
)
}
}
}
}
loadData()
}
}
override fun setInitialState() = ThermometerContract.State.Loading
override fun setInitialState() = ThermometerContract.State.Loading(null)
override fun handleEvents(event: ThermometerContract.Event) {
when(event){
@ -75,13 +36,21 @@ class ThermometerViewModel @Inject constructor(
is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowTemperatureHistory -> 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.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(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnTxSelect
@ -102,32 +71,6 @@ class ThermometerViewModel @Inject constructor(
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(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit
@ -146,7 +89,17 @@ class ThermometerViewModel @Inject constructor(
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) {
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) {
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){
val newBle = bleViewMapper.map(state.thermometer) as Ble.Thermometer
val newBle = state.thermometer
val writeRequest = Ble.Thermometer.WriteRequest(
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,
)
setState {
state.copy(
writeState = ThermometerContract.State.Display.WriteState.DisplayPreview(
setEffect {
ThermometerContract.Effect.Navigation.Write(
state.thermometer.info.serial,
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.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
get() {
return when(this){

View File

@ -13,8 +13,8 @@ import com.ramcosta.composedestinations.generated.NavGraphs
fun MainScreen() {
val navController = rememberNavController()
val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider.addNavigator(bottomSheetNavigator)
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.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.navgraphs.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.coroutines.flow.launchIn
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.Result

View File

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

View File

@ -1,6 +1,5 @@
package llc.arma.ble.app.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@ -9,12 +8,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
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.LocalView
import androidx.core.view.WindowCompat
/*val LightColorScheme = lightColorScheme(
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.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import llc.arma.ble.R
val font = FontFamily(

View File

@ -2,6 +2,7 @@ package llc.arma.ble.data.repository
import android.Manifest
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.annotation.RequiresPermission
import kotlinx.coroutines.CoroutineScope
@ -9,13 +10,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import llc.arma.ble.data.db.RotationsDao
import llc.arma.ble.data.repository.extensions.checkPermission
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.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
import 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.RemoteCharacteristic
import no.nordicsemi.kotlin.ble.client.RemoteService
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.Peripheral
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.Timer
import java.util.TimerTask
@ -56,8 +54,63 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.PI
import kotlin.math.atan
import kotlin.random.Random
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 firmwareVersionUUID: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
@ -89,25 +142,20 @@ class BleRepositoryImpl @Inject constructor(
}
@RequiresPermission(allOf = [
android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_CONNECT
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
])
override fun getBleAroundFlow() = callbackFlow {
val job = BleScanner(app)
.scan(
settings = BleScannerSettings(
includeStoredBondedDevices = false,
scanMode = BleScanMode.SCAN_MODE_LOW_LATENCY,
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 }
val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
val job = centralManager.scan {
ManufacturerData(0x0059)
}
.filter { it.peripheral.name?.contains("ArmA") == true }
.onEach {
resultList[it.device.address] = it.info
}.launchIn(CoroutineScope(Dispatchers.IO))
resultList[it.peripheral.address] = it.info
}
.launchIn(CoroutineScope(Dispatchers.IO))
val timer = Timer().apply {
schedule(object : TimerTask() {
@ -158,6 +206,7 @@ class BleRepositoryImpl @Inject constructor(
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override suspend fun getBleBySerial(
serial: String,
@ -172,35 +221,13 @@ class BleRepositoryImpl @Inject constructor(
} else {
println("Start")
val centralManager = CentralManager.Factory.native(app, scope)
val peripheral = centralManager.getPeripheralById(serial)
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
val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
try {
connection = ClientBleGatt.connect(app, serial, scope, BleGattConnectOptions(false))
val version = connection.readVersion()
val version = peripheral.readVersion()
fun BleInfo.updateState(): Ble.BleState {
return Ble.BleState(
@ -216,7 +243,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Gate(
info = initialBle,
state = initialBle.updateState(),
gateState = connection.readHostState().fold(
gateState = peripheral.readHostState().fold(
onFailure = { return Result.failure(it) },
onSuccess = { it }
)
@ -236,7 +263,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Thermometer(
info = initialBle,
state = initialBle.updateState(),
thermometerState = connection.readThermometerState(
thermometerState = peripheral.readThermometerState(
initialBle.tableStatus
).fold(
onFailure = { return Result.failure(it) },
@ -251,7 +278,7 @@ class BleRepositoryImpl @Inject constructor(
Ble.Accelerometer(
info = initialBle,
state = initialBle.updateState(),
accelerometerState = connection.readAccelState(
accelerometerState = peripheral.readAccelState(
initialBle.tableStatus,
version
).fold(
@ -270,7 +297,7 @@ class BleRepositoryImpl @Inject constructor(
return Result.failure(BleException.UnexpectedResponse)
} finally {
connection?.close()
peripheral.disconnect()
}
@ -280,21 +307,21 @@ class BleRepositoryImpl @Inject constructor(
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@OptIn(ExperimentalUnsignedTypes::class)
private suspend fun ClientBleGatt.readThermometerState(
private suspend fun Peripheral.readThermometerState(
timer: BleInfo.HistoryTableStatus
): Result<Ble.Thermometer.ThermometerState, BleException> {
val service = discoverServices().findService(serviceUUID)
val service = discoverServices()?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
var characteristic = service.findCharacteristic(temperatureReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write(DataByteArray.from(1, 1))
characteristic.write(byteArrayOf(1, 1))
delay(2_000)
val temperature = characteristic.read().value.toUByteArray().toTemperature()
val temperature = characteristic.read().toUByteArray().toTemperature()
characteristic = service.findCharacteristic(intervalReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
@ -312,11 +339,11 @@ class BleRepositoryImpl @Inject constructor(
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readHostState(
private suspend fun Peripheral.readHostState(
): Result<Ble.Gate.HostState, BleException> {
val service = discoverServices().findService(serviceUUID)
val service = discoverServices()?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
val characteristic = service.findCharacteristic(intervalReadUUID)
@ -335,11 +362,11 @@ class BleRepositoryImpl @Inject constructor(
}
@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){
it.get4byteUIntAt(0).toLong()
} else {
@ -350,11 +377,11 @@ class BleRepositoryImpl @Inject constructor(
}
@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) {
it.get4byteUIntAt(0).toLong()
} else {
@ -420,15 +447,12 @@ class BleRepositoryImpl @Inject constructor(
}
@OptIn(ExperimentalUuidApi::class)
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun Peripheral.readVersion(): Version {
return services().filter { it.isNotEmpty() }.firstOrNull()
?.firstOrNull { it.uuid.toString() == versionServiceUUID.toString() }
?.characteristics?.firstOrNull { it.uuid.toString() == firmwareVersionUUID.toString() }
return discoverServices()
?.findService(versionServiceUUID)
?.findCharacteristic(firmwareVersionUUID)
?.read()?.decodeToString()?.let {
Version.fromString(it)
} ?: Version.fromString("0.0.0-0")
@ -436,23 +460,13 @@ class BleRepositoryImpl @Inject constructor(
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private suspend fun ClientBleGatt.readVersion(): Version {
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(
private suspend fun Peripheral.readAccelState(
timer: BleInfo.HistoryTableStatus,
version: Version
): Result<Ble.Accelerometer.AccelerometerState, BleException> {
val services = discoverServices()
val service = services.findService(serviceUUID)
val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
var characteristic = service.findCharacteristic(intervalReadUUID)
@ -473,10 +487,8 @@ class BleRepositoryImpl @Inject constructor(
characteristic = service.findCharacteristic(accelerometerReadUUID)
?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write(DataByteArray.from(4))
characteristic.read().let {
val data = it.value
characteristic.write(byteArrayOf(4))
characteristic.read().let { data ->
val scale = AccelScale.fromByte(data[1]) ?: return Result.failure(
BleException.UnexpectedResponse
@ -521,7 +533,7 @@ class BleRepositoryImpl @Inject constructor(
override suspend fun getTemperatureHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> {
): Result<List<Ble.Thermometer.HistoryPoint>, BleException> {
return readThermometerHistory(serial, app)
@ -553,73 +565,62 @@ class BleRepositoryImpl @Inject constructor(
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override suspend fun writeBle(
serial: String,
request: Ble.Thermometer.WriteRequest
): 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 service = services.findService(serviceUUID) ?: return Result.failure(BleException.UnexpectedResponse)
val services = peripheral.discoverServices()
val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
request.tx?.let {
service.findCharacteristic(txWriteUUID)!!.write(
DataByteArray.from(it.sendData)
)
service.findCharacteristic(txWriteUUID)
?.write(byteArrayOf(it.sendData))
?: return Result.failure(BleException.UnexpectedResponse)
}
request.saveHistory?.let {
service.findCharacteristic(saveEnabledWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(4).apply {
mutableListOf<Byte>(4).apply {
add(if (it) 1 else 0)
}.toByteArray()
)
)
}
request.historyInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(3).apply {
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
)
)
}
service.findCharacteristic(flashWriteUUID)!!.write(
DataByteArray.from(9)
)
connection.close()
service.findCharacteristic(flashWriteUUID)!!.write(byteArrayOf(9))
peripheral.disconnect()
Result.success(Unit)
} catch (err: Throwable){
} catch (err: Throwable) {
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
connection.close()
}
} else {
Result.failure(BleException.PermissionDenied)
peripheral.disconnect()
}
@ -630,48 +631,42 @@ class BleRepositoryImpl @Inject constructor(
request: Ble.Beacon.WriteRequest
): 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))
try {
return try {
request.tx?.let {
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
txWriteUUID
)?.write(
DataByteArray.from(it.sendData)
peripheral.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(txWriteUUID)
?.write(byteArrayOf(it.sendData)
) ?: return Result.failure(BleException.UnexpectedResponse)
}
connection.discoverServices().findService(serviceUUID)?.findCharacteristic(
flashWriteUUID
)!!.write(
DataByteArray.from(9)
)
peripheral.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(flashWriteUUID)!!
.write(byteArrayOf(9))
connection.close()
peripheral.disconnect()
Result.success(Unit)
} catch (err: Throwable){
} catch (err: Throwable) {
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
connection.close()
peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
}
}
@ -680,14 +675,15 @@ class BleRepositoryImpl @Inject constructor(
request: Ble.Gate.WriteRequest
): 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 service = services.findService(serviceUUID) ?: return Result.failure(
val services = peripheral.discoverServices()
val service = services?.findService(serviceUUID) ?: return Result.failure(
BleException.UnexpectedResponse
)
@ -696,7 +692,7 @@ class BleRepositoryImpl @Inject constructor(
service.findCharacteristic(
txWriteUUID
)?.write(
DataByteArray.from(it.sendData)
byteArrayOf(it.sendData)
) ?: return Result.failure(BleException.UnexpectedResponse)
}
@ -704,54 +700,40 @@ class BleRepositoryImpl @Inject constructor(
request.interval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(3).apply {
addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
)
)
}
request.readInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(2).apply {
addAll(
(it / 1000).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
mutableListOf<Byte>(2).apply {
addAll((it / 1000).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
)
)
}
service.findCharacteristic(
flashWriteUUID
)!!.write(
DataByteArray.from(9)
byteArrayOf(9)
)
connection.close()
peripheral.disconnect()
Result.success(Unit)
} catch (err: Throwable){
} catch (err: Throwable) {
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
connection.close()
}
} else {
Result.failure(BleException.PermissionDenied)
peripheral.disconnect()
}
@ -764,31 +746,27 @@ class BleRepositoryImpl @Inject constructor(
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 =
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
return try {
try {
val services = connection.discoverServices()
val service = services.findService(serviceUUID)
val service = peripheral.discoverServices()?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
Log.d("write", request.toString())
request.tx?.let {
service.findCharacteristic(txWriteUUID)!!.write(
DataByteArray.from(it.sendData)
byteArrayOf(it.sendData)
)
}
request.saveHistorySettings?.let {
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)
if (it is Ble.Accelerometer.HistorySettings.Enabled) {
add(it.mode.sendData)
@ -796,91 +774,37 @@ class BleRepositoryImpl @Inject constructor(
}
}.toByteArray()
)
)
}
request.historyInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(3).apply {
mutableListOf<Byte>(3).apply {
addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
}.toByteArray()
)
)
}
request.readInterval?.let {
service.findCharacteristic(intervalWriteUUID)!!.write(
DataByteArray.from(
*mutableListOf<Byte>(2).apply {
addAll(
(it).toUInt().to4ByteArrayInLittleEndian().reversed().toList()
)
mutableListOf<Byte>(2).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
)
)
}
service.findCharacteristic(flashWriteUUID)!!.write(
DataByteArray.from(9)
byteArrayOf(9)
)
Result.success(Unit)
} catch (err: Throwable){
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
connection.close()
}
} else {
Result.failure(BleException.PermissionDenied)
}
}
override suspend fun changeBlePassword(
password: String,
serial: String
): Result<Unit, BleException> {
return if(app.checkPermission()) {
val connection =
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
try {
val services = connection.discoverServices()
val service = services.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
service.findCharacteristic(passwordWriteUUID)?.write(
DataByteArray(
mutableListOf(8.toByte()).apply {
addAll(password.toByteArray(Charsets.US_ASCII).toList())
}.toByteArray()
)
) ?: return Result.failure(BleException.UnexpectedResponse)
connection.close()
Result.success(Unit)
} catch (err: Throwable) {
err.printStackTrace()
@ -888,24 +812,59 @@ class BleRepositoryImpl @Inject constructor(
} finally {
connection.close()
peripheral.disconnect()
}
} else {
Result.failure(BleException.PermissionDenied)
}
}
override fun getAccelerometerMeasureBySerialFlow(
override suspend fun changeBlePassword(
password: String,
serial: String
): Result<Unit, BleException> {
val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.IO))
val peripheral = centralManager.connectPeripheral(serial)
?: return Result.failure(BleException.UnexpectedResponse)
return try {
val services = peripheral.discoverServices()
val service = services?.findService(serviceUUID)
?: return Result.failure(BleException.UnexpectedResponse)
service.findCharacteristic(passwordWriteUUID)?.write(
mutableListOf(8.toByte()).apply {
addAll(password.toByteArray(Charsets.US_ASCII).toList())
}.toByteArray()
) ?: return Result.failure(BleException.UnexpectedResponse)
peripheral.disconnect()
Result.success(Unit)
} catch (err: Throwable) {
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
peripheral.disconnect()
}
}
override suspend fun getAccelerometerMeasureBySerialFlow(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
): Flow<Result<Ble.Accelerometer.RealtimePoint, BleException>> {
) : Result<Flow<Ble.Accelerometer.RealtimePoint>, BleException> {
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.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.native
@OptIn(ExperimentalUnsignedTypes::class)
@ -41,33 +41,31 @@ fun getAccelerometerHistory(
var expectedDataSize: Int? = null
if(app.checkPermission()) {
val connection =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Default))
val peripheral = centralManager.connectPeripheral(address) ?: throw IllegalStateException()
try {
val specData = connection.discoverServices()
.findService(serviceUUID)
val specData = peripheral.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(accelerometerReadUUID)
?.let {
it.write(DataByteArray.from(4))
it.write(byteArrayOf(4))
it.read()
} ?: throw IllegalStateException()
val scale = AccelScale.fromByte(specData.value[1]) ?: throw IllegalStateException()
val mode = AccelViewMode.fromByte(specData.value[0]) ?: throw IllegalStateException()
val scale = AccelScale.fromByte(specData[1]) ?: throw IllegalStateException()
val mode = AccelViewMode.fromByte(specData[0]) ?: throw IllegalStateException()
val characteristic = connection.discoverServices()
.findService(serviceUUID)
val characteristic = peripheral.discoverServices()
?.findService(serviceUUID)
?.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))) {
@ -85,8 +83,8 @@ fun getAccelerometerHistory(
addAll(value.toList().take(2))
}.toByteArray()
characteristic.write(DataByteArray(writeData))
value = characteristic.read().value
characteristic.write(writeData)
value = characteristic.read()
while (nextPackageDataCount.toInt() != 0) {
@ -102,12 +100,16 @@ fun getAccelerometerHistory(
lastMeasureSystemTime =
System.currentTimeMillis() - ((bleRealTime - bleLastMeasureTime) * 1_000)
Log.d("dataTable",
Log.d(
"dataTable",
"bleMeasureInterval $bleMeasureInterval " +
"bleLastMeasureTime $bleLastMeasureTime " +
"bleRealTime $bleRealTime " +
"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)
@ -133,8 +135,8 @@ fun getAccelerometerHistory(
emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
emit(Result.success(ProgressState.Progress(resultPackage.size.toFloat() / expectedDataSize.toFloat())))
characteristic.write(DataByteArray.from(5))
value = characteristic.read().value
characteristic.write(byteArrayOf(5))
value = characteristic.read()
}
@ -150,9 +152,11 @@ fun getAccelerometerHistory(
)
}
}
AccelViewMode.ACCELERATION,
AccelViewMode.PEAK_ACCELERATION,
AccelViewMode.RMS -> {
AccelViewMode.RMS
-> {
resultPackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultPackage.size / 3 - 1) - it.index) * bleMeasureInterval!!),
@ -214,14 +218,10 @@ fun getAccelerometerHistory(
} 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.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.get2byteShortAt
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.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import kotlin.uuid.ExperimentalUuidApi
fun getAccelerometerRealtimeData(
suspend fun getAccelerometerRealtimeData(
app: Application,
serial: String,
accelScale: AccelScale,
@ -33,26 +38,23 @@ fun getAccelerometerRealtimeData(
fftAxis: FftAxis,
fftMode: FftViewMode,
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 =
ClientBleGatt.connect(app, serial, CoroutineScope(Dispatchers.Default))
if (peripheral == null) throw IllegalStateException()
try {
val services = connection.discoverServices()
val services = peripheral.discoverServices()
val characteristic =
services.findService(serviceUUID)
services?.findService(serviceUUID)
?.findCharacteristic(accelerometerReadUUID)
?: throw IllegalStateException()
?: return Result.failure(BleException.UnexpectedResponse)
characteristic.write(
DataByteArray.from(
byteArrayOf(
4,
accelMode.sendData,
accelScale.sendData,
@ -63,21 +65,26 @@ fun getAccelerometerRealtimeData(
)
)
characteristic.getNotifications().collect {
val value = it.value
val flow = characteristic.subscribe()
.onCompletion {
println("disable notifying")
characteristic.setNotifying(false)
peripheral.disconnect()
}
.map { value ->
val data = value.toList().chunked(2).map {
it.toByteArray().get2byteShortAt()
}
val result = when(accelMode){
val result = when (accelMode) {
VIBRATION -> {
Ble.Accelerometer.RealtimePoint.Vibration(
(value.get2byteShortAt()
.toFloat() * accelScale.k) / Short.MAX_VALUE
)
}
ANGLE -> {
Ble.Accelerometer.RealtimePoint.Angle(
x = calculateAngle(
@ -94,6 +101,7 @@ fun getAccelerometerRealtimeData(
)
)
}
ROTATIONS -> {
Ble.Accelerometer.RealtimePoint.Rotation(
angle = ((360f / 8f) * ((data[0] / 100f) + 1f)) - 45f,
@ -101,6 +109,7 @@ fun getAccelerometerRealtimeData(
turnovers = data[2]
)
}
ACCELERATION,
PEAK_ACCELERATION,
RMS -> {
@ -112,29 +121,20 @@ fun getAccelerometerRealtimeData(
}
}
emit(Result.success(result))
result
}
Result.success(flow)
} catch (err: Exception) {
peripheral?.disconnect()
err.printStackTrace()
emit(Result.failure(BleException.UnexpectedResponse))
} finally {
connection.disconnect()
connection.close()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
}
Result.failure(BleException.UnexpectedResponse)
}

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

View File

@ -2,35 +2,32 @@ package llc.arma.ble.data.repository
import android.app.Application
import android.util.Log
import androidx.annotation.RequiresPermission
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.get2byteUIntAt
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.native
import java.nio.ByteBuffer
import java.util.BitSet
import java.util.Locale
@RequiresPermission(allOf = ["android.permission.BLUETOOTH_CONNECT"])
suspend fun readTable(
characteristic: ClientBleGattCharacteristic,
characteristic: RemoteCharacteristic,
startRequest: ByteArray,
nextRequestPayload: ByteArray
): List<Byte> {
characteristic.write(DataByteArray(startRequest))
var value = characteristic.read().value
characteristic.write(startRequest)
var value = characteristic.read()
val tableResult = mutableListOf<Byte>()
do {
@ -44,8 +41,8 @@ suspend fun readTable(
tableResult.addAll(value.asList().subList(4, value.size))
characteristic.write(DataByteArray(nextRequestPayload))
value = characteristic.read().value
characteristic.write(nextRequestPayload)
value = characteristic.read()
}
@ -63,21 +60,21 @@ fun readHostHistory(
return flow {
if (app.checkPermission()) {
val centralManager = CentralManager.Factory.native(app, CoroutineScope(Dispatchers.Main))
val connection =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
val peripheral = centralManager.connectPeripheral(address)
?: throw IllegalStateException()
try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
val characteristic = peripheral?.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(hostHistoryReadUUID)
?: 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))) {
@ -91,8 +88,8 @@ fun readHostHistory(
var tableSize = value.get2byteUIntAt(0)
//Чтение без удаления
characteristic.write(DataByteArray.from(1, 0, 0, -1, -1))
val firstTableHeader = characteristic.read().value.asList()
characteristic.write(byteArrayOf(1, 0, 0, -1, -1))
val firstTableHeader = characteristic.read().asList()
dataTablePackage.addAll(firstTableHeader.subList(4, firstTableHeader.size))
secondTablePackage.addAll(
@ -112,22 +109,26 @@ fun readHostHistory(
val bleMeasureInterval = dataTablePackage.toByteArray().get4byteUIntAt(0).toLong()
val bleLastMeasureTime = dataTablePackage.toByteArray().get4byteUIntAt(4).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 {
val bits = BitSet.valueOf(bytes)
bits.clear(12, 16)
val arr = bits.toByteArray()
if(arr.isEmpty()){
if (arr.isEmpty()) {
return 0x00.toUInt()
}
if(arr.size == 1){
if (arr.size == 1) {
return arr[0].toUInt()
}
@ -135,14 +136,14 @@ fun readHostHistory(
}
fun getInnerIndex(byte: Byte): Int{
fun getInnerIndex(byte: Byte): Int {
var bits = BitSet.valueOf(byteArrayOf(byte))
bits.clear(0, 4)
bits = bits.get(4, 8)
val arr = bits.toByteArray()
if(arr.isEmpty()){
if (arr.isEmpty()) {
return 0x00
}
@ -150,13 +151,13 @@ fun readHostHistory(
}
fun getDevType(byte: Byte): Int{
fun getDevType(byte: Byte): Int {
val bits = BitSet.valueOf(byteArrayOf(byte))
bits.clear(5, 9)
val arr = bits.toByteArray()
if(arr.isEmpty()){
if (arr.isEmpty()) {
return 0x00
}
@ -164,14 +165,14 @@ fun readHostHistory(
}
fun getDevDataSize(byte: Byte): Int{
fun getDevDataSize(byte: Byte): Int {
var bits = BitSet.valueOf(byteArrayOf(byte))
bits.clear(0, 5)
bits = bits.get(5, 8)
val arr = bits.toByteArray()
if(arr.isEmpty()){
if (arr.isEmpty()) {
return 0x00
}
@ -195,7 +196,7 @@ fun readHostHistory(
if (bleIdTableCell.contentEquals(intervalEnd.hexToByteArray())) {
bleTableOffset += 2
if(periodBle.isEmpty()) bleTableOffset += 2
if (periodBle.isEmpty()) bleTableOffset += 2
periods.add(Pair(false, periodBle))
periodBle = mutableListOf()
@ -204,7 +205,7 @@ fun readHostHistory(
if (bleIdTableCell.contentEquals(intervalEndAndHit.hexToByteArray())) {
bleTableOffset += 2
if(periodBle.isEmpty()) bleTableOffset += 2
if (periodBle.isEmpty()) bleTableOffset += 2
periods.add(Pair(true, periodBle))
periodBle = mutableListOf()
@ -227,7 +228,7 @@ fun readHostHistory(
val devDataSize = getDevDataSize(devTypeByte)
bleTableOffset += 2
if(innerIndex == 0)
if (innerIndex == 0)
bleTableOffset += 2
if (devDataSize != 0) {
@ -235,7 +236,10 @@ fun readHostHistory(
dataTablePackage.drop(bleTableOffset).take(devDataSize)
.toByteArray()
//)
Log.d("payload", "${payload.joinToString(separator = " ")} ${payload.toHexString()}")
Log.d(
"payload",
"${payload.joinToString(separator = " ")} ${payload.toHexString()}"
)
bleTableOffset += devDataSize
}
@ -267,13 +271,7 @@ fun readHostHistory(
} finally {
connection.close()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
peripheral.disconnect()
}
@ -287,21 +285,20 @@ suspend fun readHostBleTable(
app: Application,
): 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 =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
return try {
try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
val characteristic = peripheral.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(hostHistoryReadUUID)
?: 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))) {
@ -311,16 +308,8 @@ suspend fun readHostBleTable(
var tableSize = value.get2byteUIntAt(0)
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
characteristic.write(DataByteArray(writeData))
value = characteristic.read().value
characteristic.write(byteArrayOf(1, 0, 0) + value)
value = characteristic.read()
Result.success(
readTable(characteristic, byteArrayOf(6), byteArrayOf(6)).chunked(8).map {
@ -342,13 +331,7 @@ suspend fun readHostBleTable(
} finally {
connection.close()
}
} else {
Result.failure(BleException.PermissionDenied)
peripheral.disconnect()
}
@ -361,25 +344,25 @@ suspend fun editBleHostTable(
app: Application,
): 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 =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
return try {
try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
val characteristic = peripheral.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(flashWriteUUID)
?: throw IllegalStateException()
characteristic.write(DataByteArray.from(12, 1))
characteristic.write(byteArrayOf(12, 1))
Log.i("ScanRecord", "write")
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()
@ -387,14 +370,12 @@ suspend fun editBleHostTable(
it.replace(":", "").lowercase(Locale.CANADA).hexToByteArray().reversed().toList()
}.toByteArray()
characteristic.write(DataByteArray.from(*command, *countPayload, *serialPayload))
characteristic.read().value.get2byteUIntAt(0).toInt()
characteristic.write(byteArrayOf(*command, *countPayload, *serialPayload))
characteristic.read().get2byteUIntAt(0).toInt()
}
characteristic.write(
DataByteArray.from(9)
)
characteristic.write(byteArrayOf(9))
delay(10_000)
@ -408,14 +389,9 @@ suspend fun editBleHostTable(
} 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import llc.arma.ble.data.repository.extensions.checkPermission
import llc.arma.ble.data.repository.extensions.get2byteUIntAt
import llc.arma.ble.data.repository.extensions.get4byteUIntAt
import llc.arma.ble.data.repository.extensions.toTemperature
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import no.nordicsemi.android.common.core.DataByteArray
import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt
import no.nordicsemi.kotlin.ble.client.android.CentralManager
@OptIn(ExperimentalUnsignedTypes::class)
fun readThermometerHistory(
suspend fun readThermometerHistory(
address: String,
app: Application,
): Flow<Result<ProgressState<List<Ble.Thermometer.HistoryPoint>>, BleException>> {
return flow {
): Result<List<Ble.Thermometer.HistoryPoint>, BleException> {
var lastMeasureSystemTime: Long? = null
@ -34,25 +27,27 @@ fun readThermometerHistory(
var expectedDataSize: Int? = null
if (app.checkPermission()) {
val peripheral =
CentralManager.Factory.connectPeripheral(
address,
app,
CoroutineScope(Dispatchers.Default)
)
val connection =
ClientBleGatt.connect(app, address, CoroutineScope(Dispatchers.Default))
return try {
try {
val characteristic = connection.discoverServices()
.findService(serviceUUID)
val characteristic = peripheral?.discoverServices()
?.findService(serviceUUID)
?.findCharacteristic(temperatureHistoryReadUUID)
?: 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))) {
emit(Result.success(ProgressState.Finished(emptyList())))
Result.success(emptyList())
} else {
@ -66,8 +61,8 @@ fun readThermometerHistory(
addAll(value.toList())
}.toByteArray()
characteristic.write(DataByteArray(writeData))
value = characteristic.read().value
characteristic.write(writeData)
value = characteristic.read()
while (nextPackageDataCount.toInt() != 0) {
@ -99,17 +94,13 @@ fun readThermometerHistory(
expectedDataSize =
nextPackageDataCount.toInt() + resultTemperaturePackage.size
emit(Result.success(ProgressState.Progress(0f / expectedDataSize.toFloat())))
emit(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize.toFloat())))
characteristic.write(DataByteArray.from(5))
value = characteristic.read().value
characteristic.write(byteArrayOf(5))
value = characteristic.read()
}
emit(
Result.success(
ProgressState.Finished(
resultTemperaturePackage.withIndex().map {
Ble.Thermometer.HistoryPoint(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
@ -117,28 +108,20 @@ fun readThermometerHistory(
)
}
)
)
)
}
} catch (err: Throwable) {
emit(Result.failure(BleException.UnexpectedResponse))
err.printStackTrace()
Result.failure(BleException.UnexpectedResponse)
} finally {
connection.close()
}
} else {
emit(Result.failure(BleException.PermissionDenied))
}
peripheral?.disconnect()
}

View File

@ -8,7 +8,7 @@ import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
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
@ -63,7 +63,7 @@ val FftViewMode.sendData: Byte
}
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
@ -79,7 +79,7 @@ val AccelViewMode.sendData: Byte
}
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

View File

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

View File

@ -1,6 +1,8 @@
package llc.arma.ble.domain.model
import kotlinx.serialization.Serializable
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.AccelViewMode
@ -15,18 +17,22 @@ sealed class Ble(
val accelerometerState: AccelerometerState
): Ble(info, state) {
@Serializable
sealed class HistorySettings {
@Serializable
data class Enabled(
val scale: AccelScale,
val mode: AccelViewMode,
val detailed: Boolean
) : HistorySettings()
@Serializable
data object Disabled : HistorySettings()
}
@Serializable
data class WriteRequest(
val tx: BleState.TX?,
val saveHistorySettings: HistorySettings?,
@ -99,6 +105,26 @@ sealed class Ble(
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(
@ -106,10 +132,30 @@ sealed class Ble(
state: BleState,
) : Ble(info, state){
@Serializable
data class WriteRequest(
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(
@ -129,12 +175,33 @@ sealed class Ble(
val readInterval: Long
)
@Serializable
data class WriteRequest(
val tx: BleState.TX?,
val interval: 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(
@ -154,12 +221,33 @@ sealed class Ble(
val historyInterval: Long
)
@Serializable
data class WriteRequest(
val tx: BleState.TX?,
val saveHistory: Boolean?,
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(

View File

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

View File

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

View File

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

View File

@ -12,7 +12,9 @@ class GetTemperatureHistoryBySerial @Inject constructor(
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)

View File

@ -45,4 +45,6 @@ dependencies {
implementation(libs.scanner)
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