diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..9a31328 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a64e1a1..a2facca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,7 +55,7 @@ dependencies { implementation 'androidx.activity:activity-compose:1.3.1' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.compose.material3:material3:1.0.0-alpha11' + implementation 'androidx.compose.material3:material3:1.1.0-beta01' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -73,6 +73,12 @@ dependencies { kapt('com.google.dagger:hilt-android-compiler:2.45') kapt("androidx.hilt:hilt-compiler:1.0.0") - implementation "androidx.datastore:datastore-preferences:1.0.0" + implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta" + + implementation "com.chargemap.compose:numberpicker:1.0.3" + + implementation "com.patrykandpatrick.vico:core:1.6.4" + implementation "com.patrykandpatrick.vico:compose:1.6.4" + implementation "com.patrykandpatrick.vico:compose-m3:1.6.4" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d29aff..726a9f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt index f32970d..07ce57b 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/MainActivity.kt @@ -4,9 +4,11 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat import dagger.hilt.android.AndroidEntryPoint import llc.arma.ble.app.ui.screen.main.MainScreen import llc.arma.ble.app.ui.theme.BleTheme @@ -15,10 +17,13 @@ import llc.arma.ble.app.ui.theme.BleTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { BleTheme { Surface( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().navigationBarsPadding(), color = MaterialTheme.colorScheme.background ) { MainScreen() diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt new file mode 100644 index 0000000..ac08ef3 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleMapper.kt @@ -0,0 +1,37 @@ +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 { + + 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) + ) + ) + } + is Ble.Thermometer -> { + BleView.Thermometer( + info = input.info, + state = BleView.BleState( + tx = txMapper.map(input.state.tx) + ), + thermometerState = BleView.Thermometer.ThermometerState( + temperature = BleView.Thermometer.ThermometerState.TemperatureState(input.thermometerState.temperature, false), + historyInterval = input.thermometerState.historyInterval, + saveHistory = input.thermometerState.saveHistory + ) + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt new file mode 100644 index 0000000..0e622ab --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/BleViewMapper.kt @@ -0,0 +1,37 @@ +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 { + + 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) + ) + ) + } + is BleView.Thermometer -> { + Ble.Thermometer( + info = input.info, + state = Ble.BleState( + tx = txMapper.map(input.state.tx) + ), + thermometerState = Ble.Thermometer.ThermometerState( + temperature = input.thermometerState.temperature.value, + historyInterval = input.thermometerState.historyInterval, + saveHistory = input.thermometerState.saveHistory + ) + ) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/Mapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/Mapper.kt new file mode 100644 index 0000000..715aaae --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/Mapper.kt @@ -0,0 +1,9 @@ +package llc.arma.ble.app.ui.mapper + +interface Mapper { + + fun map(input: I): O + + fun map(input: List): List = input.map { map(it) } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/TxMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/TxMapper.kt new file mode 100644 index 0000000..bd824fb --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/TxMapper.kt @@ -0,0 +1,23 @@ +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 { + + 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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/mapper/TxViewMapper.kt b/app/src/main/java/llc/arma/ble/app/ui/mapper/TxViewMapper.kt new file mode 100644 index 0000000..838e768 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/mapper/TxViewMapper.kt @@ -0,0 +1,24 @@ +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 { + + 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 + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt new file mode 100644 index 0000000..ddf1dfb --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/model/BleView.kt @@ -0,0 +1,62 @@ +package llc.arma.ble.app.ui.model + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import llc.arma.ble.domain.model.BleInfo + +sealed class BleView( + val info: BleInfo +) { + + 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 mutableStateOf(historyInterval) + + } + + } + + class BleState( + tx: TX + ){ + + 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) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt new file mode 100644 index 0000000..647aec4 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/BleInfoView.kt @@ -0,0 +1,154 @@ +package llc.arma.ble.app.ui.screen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import llc.arma.ble.domain.model.BleInfo + +@Composable +fun BleInfoView( + bleInfo: BleInfo +) { + + Surface( + modifier = Modifier.padding(bottom = 16.dp), + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surfaceVariant + ) { + + Column( + modifier = Modifier.padding(8.dp) + ) { + + Column() { + + BleInfoItem( + icon = { + Icon( + imageVector = when(bleInfo.type){ + BleInfo.Type.BEACON -> Icons.Rounded.Nfc + BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat + }, + contentDescription = null + ) + }, + title = "Тип метки", + subtitle = when(bleInfo.type){ + BleInfo.Type.BEACON -> "Маяк" + BleInfo.Type.THERMOMETER -> "Термодатчик" + } + ) + + SpecDivider() + + BleInfoItem( + icon = { + Icon( + imageVector = Icons.Rounded.ShortText, + contentDescription = null + ) + }, + title = "Наименование", + subtitle = bleInfo.name + ) + + SpecDivider() + + BleInfoItem( + icon = { + Icon( + imageVector = Icons.Rounded.Key, + contentDescription = null + ) + }, + title = "Адрес", + subtitle = bleInfo.serial + ) + + SpecDivider() + + BleInfoItem( + icon = { + Icon( + imageVector = Icons.Rounded.BatteryFull, + contentDescription = null + ) + }, + title = "Заряд аккумулятора", + subtitle = "${bleInfo.batteryLevel} %" + ) + + } + + } + + } + +} + +@Composable +private fun ColumnScope.SpecDivider(){ + + Spacer(modifier = Modifier.height(12.dp)) + + Divider() + + Spacer(modifier = Modifier.height(12.dp)) + +} + +@Composable +private fun BleInfoItem( + icon: @Composable () -> Unit, + title: String, + subtitle: String +){ + + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Surface( + modifier = Modifier.size(40.dp), + shape = CircleShape + ) { + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ){ + + icon() + + } + + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = title + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = subtitle + ) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt index 192bcdc..c541f3f 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconContract.kt @@ -9,6 +9,10 @@ class BeaconContract { sealed class Event : ViewEvent { + data class OnBleChanged( + val ble: Ble.Beacon + ) : Event() + data class OnTxChanged(val tx: Int) : Event() object OnNavigateUpClicked : Event() diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt index 8e77bc9..708c882 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconScreen.kt @@ -1,21 +1,33 @@ package llc.arma.ble.app.ui.screen.beacon +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip 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.screen.BleInfoView +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.model.BleInfo +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BeaconScreen( + ble: Ble.Beacon, onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit ) { @@ -30,6 +42,10 @@ fun BeaconScreen( }.launchIn(this) } + LaunchedEffect(ble){ + viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble)) + } + Column { CenterAlignedTopAppBar( @@ -47,12 +63,12 @@ fun BeaconScreen( ) }, title = { - + if (state is BeaconContract.State.Display) Text(text = state.beacon.info.name) } ) when(state){ - is BeaconContract.State.Display -> DisplayState() + is BeaconContract.State.Display -> DisplayState(state.beacon) is BeaconContract.State.Loading -> LoadingState() } @@ -73,27 +89,96 @@ private fun LoadingState(){ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DisplayState(){ +private fun DisplayState(ble: Ble.Beacon){ - Surface( - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - shape = CircleShape, - color = MaterialTheme.colorScheme.primaryContainer, - onClick = { + Column { - } - ) { + LazyColumn( + modifier = Modifier.weight(1f), + content = { - Box(modifier = Modifier.fillMaxSize()) { + item { - Text( - modifier = Modifier.align(Alignment.Center), - color = MaterialTheme.colorScheme.background, - style = MaterialTheme.typography.labelLarge, - text = "Сохранить" - ) + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + BleInfoView(bleInfo = ble.info) + } + + } + + item { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ){ + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Мощность" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "-40 db" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + } + + } + + ) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Сохранить" + ) + + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt index dea4b4b..d3a4b99 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/beacon/BeaconViewModel.kt @@ -8,32 +8,20 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.model.BleInfo import javax.inject.Inject @HiltViewModel class BeaconViewModel @Inject constructor( - savedStateHandle: SavedStateHandle ) : BaseViewModel() { - init { - - savedStateHandle.get("serial")?.let { - CoroutineScope(Dispatchers.IO).launch { - delay(5000) - setState { - BeaconContract.State.Display(Ble.Beacon()) - } - } - } - - } - override fun setInitialState() = BeaconContract.State.Loading 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) } } @@ -51,4 +39,15 @@ class BeaconViewModel @Inject constructor( } + private fun reduce( + state: BeaconContract.State, + event: BeaconContract.Event.OnBleChanged + ) { + setState { + BeaconContract.State.Display( + event.ble + ) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt index d2ab390..50c61d9 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListScreen.kt @@ -1,21 +1,30 @@ package llc.arma.ble.app.ui.screen.ble import androidx.compose.foundation.clickable -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.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.BatteryFull +import androidx.compose.material.icons.rounded.NetworkCell +import androidx.compose.material.icons.rounded.Nfc +import androidx.compose.material.icons.rounded.Thermostat +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import llc.arma.ble.domain.model.BleInfo +@OptIn(ExperimentalMaterial3Api::class) @Composable fun BleListScreen( onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit @@ -34,7 +43,14 @@ fun BleListScreen( Column { + CenterAlignedTopAppBar( + title = { + Text(text = "Arma BLE") + } + ) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize() ) { @@ -56,24 +72,102 @@ fun BleListScreen( } @Composable -fun BleItem( +private fun ItemIcon( + image: @Composable BoxScope.() -> Unit +){ + + Surface( + modifier = Modifier.size(40.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = CircleShape + ) { + Box(modifier = Modifier.fillMaxSize()) { + + image() + + } + } + +} + +@Composable +private fun BleItem( ble: BleInfo, onClick: () -> Unit ){ Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) .clickable { onClick() } + .padding(vertical = 8.dp, horizontal = 16.dp) ) { - Text(text = ble.rssi.toString()) + ItemIcon { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = when(ble.type){ + BleInfo.Type.BEACON -> Icons.Rounded.Nfc + BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat + }, + contentDescription = null + ) + } Column { + Text(text = ble.name) - Text(text = ble.serial) - Text(text = ble.uuid) - Text(text = ble.batteryLevel.toString()) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ble.serial + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.alpha(0.7f) + ) { + + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Rounded.NetworkCell, + contentDescription = null + ) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ble.rssi.toString() + " dBm" + ) + + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.alpha(0.7f) + ) { + + Icon( + modifier = Modifier.size(16.dp), + imageVector = Icons.Rounded.BatteryFull, + contentDescription = null + ) + + Text( + style = MaterialTheme.typography.bodyMedium, + text = ble.batteryLevel.toString() + " %" + ) + + } + + } + } } diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt index 28edb6c..d082ddc 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/ble/BleListViewModel.kt @@ -1,5 +1,6 @@ package llc.arma.ble.app.ui.screen.ble +import android.util.Log import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt index f3dff6a..6fa7094 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionContract.kt @@ -3,7 +3,9 @@ package llc.arma.ble.app.ui.screen.connection 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.beacon.BeaconContract +import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.usecase.GetBleBySerial @@ -11,9 +13,15 @@ class ConnectionContract { sealed class Event : ViewEvent { + object OnNavigateUp : Event() + data class OnBeaconNavigationEvent( val event: BeaconContract.Effect.Navigation - ) + ) : Event() + + data class OnThermometerNavigationEvent( + val event: ThermometerContract.Effect.Navigation + ) : Event() } @@ -25,18 +33,14 @@ class ConnectionContract { val exception: GetBleBySerial.GetBleException ) : State() + data class Display( + val ble: Ble + ) : State() + } sealed class Effect : ViewSideEffect { - sealed class ChildNavigation : Effect() { - - data class NavigateToBeacon( - val ble: Ble - ) : ChildNavigation() - - } - sealed class Navigation : Effect() { object NavigateUp : Navigation() diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt index dc710af..b9cb9e7 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionScreen.kt @@ -1,15 +1,30 @@ package llc.arma.ble.app.ui.screen.connection -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material3.* 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 kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.app.ui.screen.BleInfoView +import llc.arma.ble.app.ui.screen.beacon.BeaconScreen +import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract +import llc.arma.ble.app.ui.screen.thermometer.ThermometerScreen +import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.usecase.GetBleBySerial +@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable fun ConnectionScreen( onNavigationEvent: (ConnectionContract.Effect.Navigation) -> Unit @@ -18,25 +33,108 @@ fun ConnectionScreen( val viewModel = hiltViewModel() val state = viewModel.viewState.value - Column() { + LaunchedEffect("effect"){ + viewModel.effect.onEach { + when(it){ + is ConnectionContract.Effect.Navigation -> onNavigationEvent(it) + } + }.launchIn(this) + } + + Column { + + CenterAlignedTopAppBar( + navigationIcon = { + IconButton( + onClick = { + viewModel.setEvent(ConnectionContract.Event.OnNavigateUp) + }, + content = { + Icon( + imageVector = Icons.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() with + slideOutVertically { height -> -height } + fadeOut()).using( + SizeTransform(clip = false) + ) + } + ) { targetText -> + Text( + text = targetText + ) + } + + } + ) when (state) { is ConnectionContract.State.DisplayException -> DisplayException(state.exception) is ConnectionContract.State.Loading -> LoadingState() + is ConnectionContract.State.Display -> { + when(state.ble){ + is Ble.Beacon -> {}/*BeaconScreen( + ble = state.ble, + onNavigationEvent = { + viewModel.setEvent(ConnectionContract.Event.OnBeaconNavigationEvent(it)) + } + )*/ + is Ble.Thermometer -> { + + Column(modifier = Modifier.weight(1f)) { + + ThermometerScreen( + ble = state.ble, + onNavigationEvent = { + viewModel.setEvent( + ConnectionContract.Event.OnThermometerNavigationEvent( + it + ) + ) + } + ) + + } + + } + } + + } } } } + @Composable private fun LoadingState(){ - Box(modifier = Modifier.fillMaxSize()){ - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + Column { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } } } @Composable -private fun DisplayException(exception: GetBleBySerial.GetBleException){ +private fun DisplayException( + exception: GetBleBySerial.GetBleException +){ + + Column { + + } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt index 2add5cc..cf135ea 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/connection/ConnectionViewModel.kt @@ -5,14 +5,23 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import llc.arma.ble.app.ui.common.BaseViewModel +import llc.arma.ble.app.ui.mapper.BleMapper +import llc.arma.ble.app.ui.mapper.BleViewMapper +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.app.ui.screen.beacon.BeaconContract +import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.usecase.GetBleBySerial +import llc.arma.ble.domain.usecase.WriteBle import javax.inject.Inject @HiltViewModel class ConnectionViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - getBleBySerial: GetBleBySerial + getBleBySerial: GetBleBySerial, + private val writeBle: WriteBle, + private val bleMapper: BleMapper, + private val bleViewMapper: BleViewMapper ) : BaseViewModel() { init { @@ -23,12 +32,13 @@ class ConnectionViewModel @Inject constructor( viewModelScope.launch { getBleBySerial(serial).fold( onSuccess = { - setEffect { - when(it){ - is Ble.Beacon -> ConnectionContract.Effect.ChildNavigation.NavigateToBeacon(it) - is Ble.Thermometer -> TODO() - } + + setState { + ConnectionContract.State.Display( + ble = it + ) } + }, onFailure = { setState { @@ -46,8 +56,48 @@ class ConnectionViewModel @Inject constructor( override fun setInitialState() = ConnectionContract.State.Loading override fun handleEvents(event: ConnectionContract.Event) { - TODO("Not yet implemented") + when(event){ + is ConnectionContract.Event.OnBeaconNavigationEvent -> reduce(viewState.value, event) + is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event) + is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event) + } } + private fun reduce( + state: ConnectionContract.State, + event: ConnectionContract.Event.OnBeaconNavigationEvent + ) { + when(event.event){ + BeaconContract.Effect.Navigation.NavigateUp -> { + setEffect { + ConnectionContract.Effect.Navigation.NavigateUp + } + } + } + } + + private fun reduce( + state: ConnectionContract.State, + event: ConnectionContract.Event.OnThermometerNavigationEvent + ) { + when(event.event){ + ThermometerContract.Effect.Navigation.NavigateUp -> { + setEffect { + ConnectionContract.Effect.Navigation.NavigateUp + } + } + } + } + + private fun reduce( + state: ConnectionContract.State, + event: ConnectionContract.Event.OnNavigateUp + ) { + + setEffect { + ConnectionContract.Effect.Navigation.NavigateUp + } + + } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt index 74485e8..a0c45b4 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/main/MainScreen.kt @@ -29,7 +29,7 @@ fun MainScreen() { BleListScreen( onNavigationEvent = { when(it){ - is BleListContract.Effect.Navigation.NavigateToBle -> controller.navigate("beacon/${it.serial}") + is BleListContract.Effect.Navigation.NavigateToBle -> controller.navigate("connection/${it.serial}") } } ) @@ -52,16 +52,8 @@ fun MainScreen() { } ) - composable( - route = "thermometer", - content = { - - ThermometerScreen() - - } - ) - } + ) } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt new file mode 100644 index 0000000..8f78d73 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerContract.kt @@ -0,0 +1,99 @@ +package llc.arma.ble.app.ui.screen.thermometer + +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 { + + object OnWriteBle : Event() + + object OnHideWriteBlePreview : Event() + + object OnShowWriteBlePreview : Event() + + object OnShowTemperatureHistory : Event() + + object OnHideTemperatureHistory : Event() + + data class OnSaveHistoryChanged( + val saveHistory: Boolean + ) : Event() + + object OnPowerEdit : Event() + + data class OnPowerChanged( + val tx: BleView.BleState.TX + ) : Event() + + object OnSaveIntervalEdit : Event() + + data class OnSaveIntervalChanged( + val interval: Long + ) : Event() + + data class OnBleChanged( + val ble: Ble.Thermometer + ) : Event() + + data class OnTxChanged(val tx: Int) : Event() + + object OnNavigateUpClicked : Event() + + } + + sealed class State : ViewState { + + object Loading : 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() + + object Success : WriteState() + + } + + } + + } + + sealed class Effect : ViewSideEffect { + + object ShowTemperatureHistory : Effect() + + object HideTemperatureHistory : Effect() + + object ShowIntervalPicker : Effect() + + object HideIntervalPicker : Effect() + + object ShowPowerPicker : Effect() + + object HidePowerPicker : Effect() + + sealed class Navigation : Effect() { + + object NavigateUp : Navigation() + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt index 256452f..2694001 100644 --- a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerScreen.kt @@ -1,7 +1,499 @@ package llc.arma.ble.app.ui.screen.thermometer -import androidx.compose.runtime.Composable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import llc.arma.ble.app.ui.screen.thermometer.view.* +import llc.arma.ble.domain.model.Ble +enum class SheetPage { + INTERVAL, POWER, TEMPERATURE_HISTORY +} + +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun ThermometerScreen() { +fun ThermometerScreen( + ble: Ble.Thermometer, + onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit +) { + + var sheetPage by remember { + mutableStateOf(null) + } + + val viewModel = hiltViewModel() + val state = viewModel.viewState.value + + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val writeSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + LaunchedEffect("effect"){ + viewModel.effect.onEach { + when(it){ + is ThermometerContract.Effect.Navigation -> onNavigationEvent(it) + is ThermometerContract.Effect.HideIntervalPicker -> launch { + bottomSheetState.hide() + sheetPage = null + } + is ThermometerContract.Effect.ShowIntervalPicker -> launch { + sheetPage = SheetPage.INTERVAL + } + is ThermometerContract.Effect.HidePowerPicker -> launch { + bottomSheetState.hide() + sheetPage = null + } + is ThermometerContract.Effect.ShowPowerPicker -> launch { + sheetPage = SheetPage.POWER + } + is ThermometerContract.Effect.HideTemperatureHistory -> launch { + bottomSheetState.hide() + sheetPage = null + } + is ThermometerContract.Effect.ShowTemperatureHistory -> launch { + sheetPage = SheetPage.TEMPERATURE_HISTORY + } + } + }.launchIn(this) + + } + + LaunchedEffect(ble){ + viewModel.setEvent(ThermometerContract.Event.OnBleChanged(ble)) + } + + Column { + + when(state){ + is ThermometerContract.State.Display -> { + DisplayState( + ble = state.thermometer, + onEvent = { + viewModel.setEvent(it) + } + ) + } + is ThermometerContract.State.Loading -> LoadingState() + } + + } + + sheetPage?.let { + + Column() { + + ModalBottomSheet( + modifier = Modifier, + sheetState = bottomSheetState, + onDismissRequest = { + sheetPage = null + }, + content = { + + Column() { + + if (state is ThermometerContract.State.Display) { + + when (sheetPage) { + SheetPage.INTERVAL -> { + IntervalEdit( + state = state.thermometer, + onEvent = { + viewModel.setEvent(it) + } + ) + } + SheetPage.POWER -> { + PowerEdit( + state = state.thermometer, + onEvent = { + viewModel.setEvent(it) + } + ) + } + SheetPage.TEMPERATURE_HISTORY -> TemperatureHistory(state.thermometer.info) + null -> {} + } + + } + + Spacer(modifier = Modifier.height(48.dp)) + + } + + } + + ) + + + + } + + } + + if(state is ThermometerContract.State.Display){ + + state.writeState?.let { + + val scope = rememberCoroutineScope() + + ModalBottomSheet( + modifier = Modifier, + containerColor = MaterialTheme.colorScheme.surface, + sheetState = writeSheetState, + onDismissRequest = { + viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview) + }, + content = { + + Column() { + + when (it) { + is ThermometerContract.State.Display.WriteState.DisplayPreview -> { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Записать изменения?", + style = MaterialTheme.typography.titleLarge + ) + + it.writeRequest.tx?.let { + Box( + modifier = Modifier.padding( + vertical = 8.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} db" + ) + + } + + } + + } + } + + it.writeRequest.saveHistory?.let { + + } + + it.writeRequest.historyInterval?.let { + + Box( + modifier = Modifier.padding( + vertical = 8.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 = "${ state.origin.thermometerState.historyInterval / 1000 / 60 / 60 } ч. -> ${it / 1000 / 60 / 60} ч." + ) + + } + + } + + } + + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + viewModel.setEvent(ThermometerContract.Event.OnWriteBle) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Записать" + ) + + } + + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + onClick = { + scope.launch { + writeSheetState.hide() + viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview) + } + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + text = "Отменить" + ) + + } + + } + + + } + is ThermometerContract.State.Display.WriteState.Writing -> { + + Box { + + Column() { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Запись", + style = MaterialTheme.typography.titleLarge + ) + + Column( + modifier = Modifier.alpha(0.6f) + ) { + + it.writeRequest.tx?.let { + Box( + modifier = Modifier.padding( + vertical = 8.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} db" + ) + + } + + } + + } + } + + it.writeRequest.saveHistory?.let { + + } + + it.writeRequest.historyInterval?.let { + + Box( + modifier = Modifier.padding( + vertical = 8.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 = "${state.origin.thermometerState.historyInterval / 1000 / 60 / 60} ч. -> ${it / 1000 / 60 / 60} ч." + ) + + } + + } + + } + + } + + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + onClick = { + scope.launch { + writeSheetState.hide() + viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview) + } + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + text = "Отменить" + ) + + } + + } + + } + + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(bottom = 48.dp) + ) + + } + + } + ThermometerContract.State.Display.WriteState.Success -> { + + Box { + + Column { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Запись завершена", + style = MaterialTheme.typography.titleLarge + ) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + onClick = { + scope.launch { + writeSheetState.hide() + viewModel.setEvent(ThermometerContract.Event.OnHideWriteBlePreview) + } + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelLarge, + text = "Ок" + ) + + } + + } + + } + + } + + } + + } + + Spacer(modifier = Modifier.height(48.dp)) + + } + + } + ) + + } + + } + + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt new file mode 100644 index 0000000..dd404cb --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/ThermometerViewModel.kt @@ -0,0 +1,233 @@ +package llc.arma.ble.app.ui.screen.thermometer + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import llc.arma.ble.app.ui.common.BaseViewModel +import llc.arma.ble.app.ui.mapper.BleMapper +import llc.arma.ble.app.ui.mapper.BleViewMapper +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.usecase.WriteBle +import javax.inject.Inject + +@HiltViewModel +class ThermometerViewModel @Inject constructor( + private val bleMapper: BleMapper, + private val bleViewMapper: BleViewMapper, + private val writeBle: WriteBle +) : BaseViewModel() { + + override fun setInitialState() = ThermometerContract.State.Loading + + override fun handleEvents(event: ThermometerContract.Event) { + when(event){ + is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event) + is ThermometerContract.Event.OnTxChanged -> reduce(viewState.value, event) + is ThermometerContract.Event.OnBleChanged -> reduce(viewState.value, event) + is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event) + is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event) + is ThermometerContract.Event.OnPowerChanged -> reduce(viewState.value, event) + is ThermometerContract.Event.OnPowerEdit -> reduce(viewState.value, event) + is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event) + is ThermometerContract.Event.OnHideTemperatureHistory -> 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) + } + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnNavigateUpClicked + ) { + setEffect { ThermometerContract.Effect.Navigation.NavigateUp } + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnTxChanged + ) { + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnBleChanged + ) { + 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 + ) { + setEffect { + ThermometerContract.Effect.ShowIntervalPicker + } + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnSaveIntervalChanged + ) { + if(state is ThermometerContract.State.Display) { + + state.thermometer.thermometerState.historyInterval = event.interval + + } + + setEffect { + ThermometerContract.Effect.HideIntervalPicker + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnPowerEdit + ) { + setEffect { + ThermometerContract.Effect.ShowPowerPicker + } + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnPowerChanged + ) { + if(state is ThermometerContract.State.Display) { + + state.thermometer.state.tx = event.tx + + } + + setEffect { + ThermometerContract.Effect.HidePowerPicker + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnSaveHistoryChanged + ) { + if(state is ThermometerContract.State.Display) { + + state.thermometer.thermometerState.saveHistory = event.saveHistory + + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnShowTemperatureHistory + ) { + + setEffect { + ThermometerContract.Effect.ShowTemperatureHistory + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnHideTemperatureHistory + ) { + + setEffect { + ThermometerContract.Effect.HideTemperatureHistory + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnShowWriteBlePreview + ) { + + if(state is ThermometerContract.State.Display){ + + val newBle = bleViewMapper.map(state.thermometer) as Ble.Thermometer + + val writeRequest = Ble.Thermometer.WriteRequest( + tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx, + saveHistory = if(newBle.thermometerState.saveHistory == state.origin.thermometerState.saveHistory) null else newBle.thermometerState.saveHistory, + historyInterval = if(newBle.thermometerState.historyInterval == state.origin.thermometerState.historyInterval) null else newBle.thermometerState.historyInterval, + ) + + setState { + state.copy( + writeState = ThermometerContract.State.Display.WriteState.DisplayPreview( + writeRequest + ) + ) + } + + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnWriteBle + ) { + + if(state is ThermometerContract.State.Display){ + + state.writeState?.let { + + if(it is ThermometerContract.State.Display.WriteState.DisplayPreview) { + + viewModelScope.launch { + + setState { + state.copy( + writeState = ThermometerContract.State.Display.WriteState.Writing(it.writeRequest) + ) + } + + writeBle(state.thermometer.info.serial, it.writeRequest) + + setState { + state.copy( + writeState = ThermometerContract.State.Display.WriteState.Success + ) + } + + } + + } + + } + + } + + } + + private fun reduce( + state: ThermometerContract.State, + event: ThermometerContract.Event.OnHideWriteBlePreview + ) { + + if(state is ThermometerContract.State.Display){ + + setState { + state.copy( + writeState = null + ) + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt new file mode 100644 index 0000000..f4a5a2d --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/DisplayState.kt @@ -0,0 +1,284 @@ +package llc.arma.ble.app.ui.screen.thermometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowRight +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.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.thermometer.ThermometerContract + +@Composable +fun DisplayState( + onEvent: (ThermometerContract.Event) -> Unit, + ble: BleView.Thermometer +) { + + Column() { + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(1f) + ) { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + BleInfoView(bleInfo = ble.info) + } + + Column( + modifier = Modifier, + content = { + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(ThermometerContract.Event.OnPowerEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Мощность" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${ble.state.tx.value} db" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Температура" + ) + + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${ble.thermometerState.temperature.value} °C" + ) + + } + + if (ble.thermometerState.temperature.loading) { + + CircularProgressIndicator() + + } else { + + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + + } + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Сохранять историю измерений" + ) + + } + + Switch( + checked = ble.thermometerState.saveHistory, + onCheckedChange = { + onEvent(ThermometerContract.Event.OnSaveHistoryChanged(it)) + } + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(ThermometerContract.Event.OnSaveIntervalEdit) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "Интервал измерний" + ) + Text( + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium, + text = "${ble.thermometerState.historyInterval / 1000 / 60 / 60} ч." + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null + ) + + } + + } + + Box( + modifier = Modifier.padding( + vertical = 8.dp, + horizontal = 8.dp + ) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + onEvent(ThermometerContract.Event.OnShowTemperatureHistory) + } + .padding(8.dp) + ) { + + Column( + modifier = Modifier.weight(1f) + ) { + + Text( + text = "График измерений" + ) + + } + + Icon( + imageVector = Icons.Rounded.KeyboardArrowRight, + contentDescription = null + ) + + } + + } + + } + ) + + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent(ThermometerContract.Event.OnShowWriteBlePreview) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Сохранить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt new file mode 100644 index 0000000..8aef3a3 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/IntervalEdit.kt @@ -0,0 +1,95 @@ +package llc.arma.ble.app.ui.screen.thermometer.view + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.chargemap.compose.numberpicker.NumberPicker +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract + +@Composable +fun IntervalEdit( + state: BleView.Thermometer, + onEvent: (ThermometerContract.Event) -> Unit, +){ + + var value by remember(state.thermometerState.historyInterval) { + mutableStateOf((state.thermometerState.historyInterval / 1000 / 60 / 60).toInt()) + } + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Интервал измерений", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + + NumberPicker( + dividersColor = MaterialTheme.colorScheme.primary, + value = value, + onValueChange = { + value = it + }, + textStyle = MaterialTheme.typography.titleMedium, + range = 1..100 + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "ч.", + style = MaterialTheme.typography.titleMedium + ) + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent( + ThermometerContract.Event.OnSaveIntervalChanged( + value.toLong() * 1000 * 60 * 60 + ) + ) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Применить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/LoadingState.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/LoadingState.kt new file mode 100644 index 0000000..e00544c --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/LoadingState.kt @@ -0,0 +1,19 @@ +package llc.arma.ble.app.ui.screen.thermometer.view + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun LoadingState(){ + + Box(modifier = Modifier.fillMaxSize()) { + + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/PowerEdit.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/PowerEdit.kt new file mode 100644 index 0000000..038aacd --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/PowerEdit.kt @@ -0,0 +1,96 @@ +package llc.arma.ble.app.ui.screen.thermometer.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import llc.arma.ble.app.ui.model.BleView +import llc.arma.ble.app.ui.screen.thermometer.ThermometerContract + +@Composable +fun PowerEdit( + state: BleView.Thermometer, + onEvent: (ThermometerContract.Event) -> Unit, +){ + + var value by remember(state.state.tx) { + mutableStateOf(state.state.tx) + } + + Column( + modifier = Modifier + ) { + + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = "Мощность", + style = MaterialTheme.typography.titleLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + BleView.BleState.TX.values().forEach { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { value = it } + .padding(4.dp) + ) { + + RadioButton( + selected = it == value, + onClick = { value = it } + ) + + Text(text = it.value.toString() + " db") + + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + .height(50.dp), + shape = CircleShape, + color = MaterialTheme.colorScheme.primaryContainer, + onClick = { + onEvent( + ThermometerContract.Event.OnPowerChanged( + value + ) + ) + } + ) { + + Box(modifier = Modifier.fillMaxSize()) { + + Text( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.labelLarge, + text = "Применить" + ) + + } + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt new file mode 100644 index 0000000..21cea30 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/app/ui/screen/thermometer/view/TemperatureHistory.kt @@ -0,0 +1,216 @@ +package llc.arma.ble.app.ui.screen.thermometer.view + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.column.columnChart +import com.patrykandpatrick.vico.compose.chart.line.lineChart +import com.patrykandpatrick.vico.core.chart.composed.plus +import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer +import com.patrykandpatrick.vico.core.entry.FloatEntry +import com.patrykandpatrick.vico.core.entry.composed.plus +import com.patrykandpatrick.vico.core.entry.entriesOf +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import llc.arma.ble.app.ui.common.BaseViewModel +import llc.arma.ble.app.ui.common.ViewEvent +import llc.arma.ble.app.ui.common.ViewSideEffect +import llc.arma.ble.app.ui.common.ViewState +import llc.arma.ble.domain.model.BleInfo +import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial +import javax.inject.Inject +import kotlin.random.Random +import kotlin.random.nextInt +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Refresh +import com.patrykandpatrick.vico.core.axis.AxisPosition +import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter +import com.patrykandpatrick.vico.core.entry.ChartEntry +import llc.arma.ble.domain.model.Ble +import java.text.SimpleDateFormat +import java.util.* + +class TemperatureEntry( + val localDate: Long, + override val x: Float, + override val y: Float, +) : ChartEntry { + + override fun withY(y: Float) = TemperatureEntry(localDate, x, y) + +} + +val formatter = SimpleDateFormat("dd.MM.yy HH:mm", Locale.getDefault()) + +@Composable +fun TemperatureHistory( + ble: BleInfo +) { + + val viewModel = hiltViewModel() + val state = viewModel.viewState.value + + LaunchedEffect(ble.serial) { + viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial)) + } + + Column() { + + Row( + modifier = Modifier.padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Text( + modifier = Modifier.weight(1f), + text = "История измерений", + style = MaterialTheme.typography.titleLarge + ) + + IconButton( + onClick = { + viewModel.setEvent(TemperatureHistoryContract.Event.LoadHistory(ble.serial)) + }, + enabled = state is TemperatureHistoryContract.State.Display + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + + } + + Spacer(modifier = Modifier.height(16.dp)) + + when (state) { + is TemperatureHistoryContract.State.Display -> { + + val producer = state.history.mapIndexed { index, measurePoint -> + TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value) }.let { + ChartEntryModelProducer(it) + } + + val axisValueFormatter = AxisValueFormatter { value, chartValues -> + (chartValues.chartEntryModel.entries.first().getOrNull(value.toInt()) as? TemperatureEntry) + ?.localDate + ?.let { formatter.format(Date(it)) } + .orEmpty() + } + + val lineChart = lineChart() + + Box(modifier = Modifier.padding(8.dp)) { + + Chart( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.5f), + chart = lineChart, + chartModelProducer = producer, + startAxis = startAxis(), + bottomAxis = bottomAxis( + valueFormatter = axisValueFormatter, + labelRotationDegrees = 0f, + ), + ) + + } + + } + is TemperatureHistoryContract.State.Loading -> { + + Box(modifier = Modifier.padding(8.dp)) { + + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(2f), + ){ + + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + + } + + } + + } + + } + + } + +} + +class TemperatureHistoryContract { + + sealed class Event : ViewEvent { + + data class LoadHistory( + val serial: String + ) : Event() + + } + + sealed class State : ViewState { + + object Loading : State() + + data class Display( + var history : List + ) : State() + + } + + sealed class Effect : ViewSideEffect { + + } + +} + + + +@HiltViewModel +class TemperatureHistoryViewModel @Inject constructor( + private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial +) : BaseViewModel() { + + override fun setInitialState() = TemperatureHistoryContract.State.Loading + + override fun handleEvents(event: TemperatureHistoryContract.Event) { + when(event){ + is TemperatureHistoryContract.Event.LoadHistory -> reduce(viewState.value, event) + } + } + + private fun reduce( + state: TemperatureHistoryContract.State, + event: TemperatureHistoryContract.Event.LoadHistory + ) { + viewModelScope.launch { + + setState { + TemperatureHistoryContract.State.Loading + } + + val history = getTemperatureHistoryBySerial(event.serial) + + setState { + TemperatureHistoryContract.State.Display(history) + } + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt index b89debf..05d0528 100644 --- a/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt +++ b/app/src/main/java/llc/arma/ble/data/BleRepositoryImpl.kt @@ -2,34 +2,74 @@ package llc.arma.ble.data import android.Manifest import android.app.Application -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothManager -import android.bluetooth.BluetoothProfile +import android.bluetooth.* import android.bluetooth.le.ScanCallback -import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.pm.PackageManager +import android.os.Build +import android.os.ParcelUuid import android.util.Log import androidx.core.app.ActivityCompat import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.flow import llc.arma.ble.domain.Result import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.repository.BleRepository import llc.arma.ble.domain.usecase.GetBleBySerial +import java.nio.ByteBuffer import java.util.* import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +@Singleton class BleRepositoryImpl @Inject constructor( private val app: Application ) : BleRepository { + private val ScanResult.info: BleInfo + get() { + return BleInfo( + name = scanRecord?.deviceName ?: "", + serial = device.address, + batteryLevel = batteryLevel ?: 0, + rssi = rssi, + type = type + ) + } + + private val ScanResult.timerEnabled: Boolean + get() { + return scanRecord?.manufacturerSpecificData?.get(89)?.get(2) == 1.toByte() + } + + private val ScanResult.batteryLevel: Int? + get() { + return scanRecord?.manufacturerSpecificData?.get(89)?.get(1) + ?.toUByte()?.toInt() + } + + private val ScanResult.type: BleInfo.Type + get() { + return when(scanRecord?.manufacturerSpecificData?.get(89)?.get(0)?.toUByte()?.toInt()){ + 2 -> BleInfo.Type.THERMOMETER + else -> BleInfo.Type.BEACON + } + } + + private fun ByteArray.getUIntAt(idx: Int) = + ((this[idx + 3].toUInt() and 0xFFu) shl 24) or + ((this[idx + 2].toUInt() and 0xFFu) shl 16) or + ((this[idx + 1].toUInt() and 0xFFu) shl 8) or + (this[idx].toUInt() and 0xFFu) + + private val deviceCache = mutableMapOf() + override fun getBleAroundFlow(): Flow> { val resultList = mutableMapOf() @@ -51,13 +91,13 @@ class BleRepositoryImpl @Inject constructor( ) == PackageManager.PERMISSION_GRANTED ) { - resultList[result.device.address] = BleInfo( - name = result.scanRecord?.deviceName ?: "", - serial = result.device.address, - uuid = result.device.uuids?.firstOrNull()?.toString() ?: "", - batteryLevel = 1, - rssi = result.rssi - ) + if(result.scanRecord?.deviceName?.contains("ArmA") == true) { + + resultList[result.device.address] = result.info + + deviceCache[result.device.address] = result + + } } @@ -66,7 +106,16 @@ class BleRepositoryImpl @Inject constructor( } val bleScanner = app.getSystemService(BluetoothManager::class.java).adapter.bluetoothLeScanner - bleScanner.startScan(bleCallback) + bleScanner.startScan( + listOf(), + ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + .setReportDelay(0L) + .build(), + bleCallback) val timer = Timer().apply { schedule(object : TimerTask() { @@ -75,7 +124,7 @@ class BleRepositoryImpl @Inject constructor( send(resultList.values.toList()) } } - }, 0, 1000) + }, 100, 500) } awaitClose { @@ -88,72 +137,392 @@ class BleRepositoryImpl @Inject constructor( } @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun getBleBySerial(serial: String): Result = suspendCancellableCoroutine { + override suspend fun getBleBySerial( + serial: String + ): Result = suspendCancellableCoroutine { - val bluetoothManager = app.getSystemService(BluetoothManager::class.java) - val bleScanner = bluetoothManager.adapter.bluetoothLeScanner + deviceCache[serial]?.let { result -> - if (ActivityCompat.checkSelfPermission( - app, - Manifest.permission.BLUETOOTH_CONNECT - ) != PackageManager.PERMISSION_GRANTED - ) { + if (ActivityCompat.checkSelfPermission( + app, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + ) { - it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)){ + if (it.isActive) { + + val info = result.info + + val state = Ble.BleState( + tx = when (result.scanRecord?.txPowerLevel) { + -40 -> Ble.BleState.TX.MINUS_40 + -20 -> Ble.BleState.TX.MINUS_20 + -16 -> Ble.BleState.TX.MINUS_16 + -12 -> Ble.BleState.TX.MINUS_12 + -8 -> Ble.BleState.TX.MINUS_8 + -4 -> Ble.BleState.TX.MINUS_4 + 3 -> Ble.BleState.TX.PLUS_3 + 4 -> Ble.BleState.TX.PLUS_4 + else -> Ble.BleState.TX.ZERO + } + ) + + CoroutineScope(Dispatchers.IO).launch { + + val resultValue = when (info.type) { + + BleInfo.Type.BEACON -> Ble.Beacon( + info = info, + state = state + ) + + BleInfo.Type.THERMOMETER -> { + + Ble.Thermometer( + info = info, + state = state, + thermometerState = readThermometerState(result) + ) + + } + + } + + it.resume(Result.success(resultValue)) {} + + } + + } + + } else { + it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) {} + } + + } + + } + + private suspend fun readThermometerState( + record: ScanResult + ): Ble.Thermometer.ThermometerState { + + return Ble.Thermometer.ThermometerState( + temperature = readTemperature(record), + saveHistory = record.timerEnabled, + historyInterval = readHistoryInterval(record) + ) + + } + + private suspend fun readTemperature( + record: ScanResult + ): Float { + + val dataResult = readCharacteristic( + device = record.device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb") + ) + + return (dataResult[0] + dataResult[1] * 256).toFloat() / 100f + + } + + private suspend fun readHistoryInterval( + record: ScanResult + ): Long { + + writeCharacteristic( + device = record.device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + writeData = byteArrayOf(3, 0, 0, 0, 0) + ) + + val dataResult = readCharacteristic( + device = record.device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb") + ) + + return if(dataResult.size == 4){ + dataResult.getUIntAt(0).toLong() + }else{ + 0 + } + + } + + override suspend fun getTemperatureHistoryBySerial( + serial: String + ): List { + + fun ByteArray.getUIntAt(idx: Int) = + ((this[idx + 3].toUInt() and 0xFFu) shl 24) or + ((this[idx + 2].toUInt() and 0xFFu) shl 16) or + ((this[idx + 1].toUInt() and 0xFFu) shl 8) or + (this[idx].toUInt() and 0xFFu) + + deviceCache[serial]?.device?.let { device -> + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + writeData = byteArrayOf(2) + ) + + val countDataArray = readCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + ) + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + writeData = mutableListOf( + 1.toByte(), + 0.toByte(), + 0.toByte() + ).apply { + addAll(countDataArray.toList()) + }.toByteArray() + ) + + val firstPackageResponse = readCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + ) + + if(firstPackageResponse[0] == 250.toByte()){ + + val interval = firstPackageResponse.getUIntAt(2).toLong() + val lastMeasureTime = firstPackageResponse.getUIntAt(6).toLong() + val realTime = firstPackageResponse.getUIntAt(10).toLong() + + val lastMeasureSystemTime = System.currentTimeMillis() - ((realTime - lastMeasureTime) / 10_000) + + var temperatureDataArray = firstPackageResponse.asList().subList(14, firstPackageResponse.size) + + val temperaturePackage = temperatureDataArray.chunked(2).map { + (it[0] + it[1] * 256).toFloat() / 100f + }.toMutableList() + + Log.d("read", temperaturePackage.size.toString()) + + var dataCount = firstPackageResponse[1] + + while(dataCount != 0.toByte()){ + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + writeData = byteArrayOf(5) + ) + + val readResponse = readCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"), + ) + + dataCount = readResponse.get(1) + + temperatureDataArray = readResponse.toList().subList(2, readResponse.size) + + temperaturePackage.addAll( + temperatureDataArray.chunked(2).map { + (it[0] + it[1] * 256).toFloat() / 100f + } + ) + + Log.d("read",(temperatureDataArray.size / 2).toString()) + + } + + Log.d("metadata", interval.toString() + " " + lastMeasureSystemTime.toString()) + + return temperaturePackage.withIndex().map { + Ble.Thermometer.MeasurePoint( + date = lastMeasureSystemTime - (((temperaturePackage.size - 1) - it.index) * interval), + value = it.value + ) + } } } - val connected = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT) - .firstOrNull { device -> device.address == serial } + return emptyList() - if(connected != null){ + } - it.resume( - Result.success( - Ble.Beacon( - info = BleInfo( - name = connected.name, - serial = connected.address, - uuid = connected.uuids?.firstOrNull()?.toString() ?: "", - rssi = 0, - batteryLevel = 1 - ) - ) - ) - ){} + override suspend fun writeBle(ble: Ble) { + when(ble){ + is Ble.Beacon -> writeBeacon(ble) + is Ble.Thermometer -> writeThermometer(ble) + } + } - } else { + override suspend fun writeBle( + serial: String, + request: Ble.Thermometer.WriteRequest + ) { - val bleCallback = object : ScanCallback() { + deviceCache[serial]?.let { result -> - override fun onScanResult( - callbackType: Int, - result: ScanResult - ) { + request.tx?.let { writeTx(result.device, it) } - super.onScanResult(callbackType, result) + request.historyInterval?.let { writeSaveInterval(result.device, it) } - if (ActivityCompat.checkSelfPermission( + request.saveHistory?.let { writeSaveEnabled(result.device, it) } + + } + + } + + private suspend fun writeBeacon(ble: Ble.Beacon){ + + deviceCache[ble.info.serial]?.device?.let { + + writeTx(it, ble.state.tx) + + } + + } + + private suspend fun writeThermometer(ble: Ble.Thermometer){ + + deviceCache[ble.info.serial]?.device?.let { + + writeTx(it, ble.state.tx) + + writeSaveInterval(it, ble.thermometerState.historyInterval) + + } + + } + + private suspend fun writeTx( + device: BluetoothDevice, + tx: Ble.BleState.TX + ) { + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"), + writeData = byteArrayOf( + when(tx) { + 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 + } + ) + ) + + } + + private suspend fun writeSaveInterval( + device: BluetoothDevice, + interval: Long + ) { + + fun UInt.to4ByteArrayInBigEndian(): ByteArray = + (3 downTo 0).map { + (this shr (it * Byte.SIZE_BITS)).toByte() + }.reversed().toByteArray() + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"), + writeData = mutableListOf(3).apply { + addAll(interval.toUInt().to4ByteArrayInBigEndian().toList()) + }.toByteArray() + ) + + } + + private suspend fun writeSaveEnabled( + device: BluetoothDevice, + enabled: Boolean + ) { + + writeCharacteristic( + device = device, + serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"), + characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"), + writeData = mutableListOf(3).apply { + add(if(enabled) 1 else 0) + }.toByteArray() + ) + + } + + private suspend fun readCharacteristic( + device: BluetoothDevice, + serviceId: UUID, + characteristicId: UUID + ): ByteArray = suspendCoroutine { + + val callback = object : BluetoothGattCallback() { + + override fun onConnectionStateChange( + gatt: BluetoothGatt?, + status: Int, + newState: Int + ) { + + if (newState == BluetoothProfile.STATE_CONNECTED) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( app, Manifest.permission.BLUETOOTH_CONNECT ) == PackageManager.PERMISSION_GRANTED ) { + gatt?.discoverServices() + } - it.resume( - Result.success( - Ble.Beacon( - info = BleInfo( - name = result.device.name, - serial = result.device.address, - uuid = result.device.uuids?.firstOrNull()?.toString() ?: "", - rssi = result.rssi, - batteryLevel = 1 - ) - ) - ) - ){} + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + + } + + } + + override fun onServicesDiscovered( + gatt: BluetoothGatt?, + status: Int + ) { + super.onServicesDiscovered(gatt, status) + + if (status == BluetoothGatt.GATT_SUCCESS) { + + gatt?.services?.firstOrNull { service -> + service.uuid == serviceId + }?.characteristics?.firstOrNull { characteristic -> + characteristic.uuid == characteristicId + }?.let { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( + app, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + ) { + + gatt.readCharacteristic(it) + + } } @@ -161,19 +530,103 @@ class BleRepositoryImpl @Inject constructor( } - bleScanner.startScan( - listOf(ScanFilter.Builder().setDeviceAddress(serial).build()), - ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - .setReportDelay(0L) - .build(), - bleCallback - ) + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, value, status) + + it.resume(value) + + } + } + device.connectGatt(app, true, callback) + + } + + private suspend fun writeCharacteristic( + device: BluetoothDevice, + serviceId: UUID, + characteristicId: UUID, + writeData: ByteArray + ) = suspendCoroutine { + + val callback = object : BluetoothGattCallback() { + + override fun onConnectionStateChange( + gatt: BluetoothGatt?, + status: Int, + newState: Int + ) { + + if (newState == BluetoothProfile.STATE_CONNECTED) { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( + app, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + ) { + gatt?.discoverServices() + } + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + + } + + } + + override fun onServicesDiscovered( + gatt: BluetoothGatt?, + status: Int + ) { + super.onServicesDiscovered(gatt, status) + + if (status == BluetoothGatt.GATT_SUCCESS) { + + gatt?.services?.firstOrNull { service -> + service.uuid == serviceId + }?.characteristics?.firstOrNull { characteristic -> + characteristic.uuid == characteristicId + }?.let { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission( + app, + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED + ) { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeCharacteristic(it, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) + }else{ + it.value = writeData + gatt.writeCharacteristic(it) + } + + } + + } + + } + + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic?, + status: Int + ) { + super.onCharacteristicWrite(gatt, characteristic, status) + it.resume(Unit) + } + + } + + device.connectGatt(app, true, callback) + } } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt index a887e17..1ec1cb3 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/Ble.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/Ble.kt @@ -1,29 +1,53 @@ package llc.arma.ble.domain.model -import android.service.controls.templates.TemperatureControlTemplate - sealed class Ble( val info: BleInfo ) { class Beacon( - info: BleInfo + info: BleInfo, + val state: BleState ) : Ble(info){ - + class WriteRequest( + val tx: BleState.TX? + ) } class Thermometer( info: BleInfo, - val currentTemperature: Float + val state: BleState, + val thermometerState: ThermometerState ) : Ble(info) { - class TemperatureRecord( + class MeasurePoint( + val date: Long, + val value: Float + ) + + class ThermometerState( val temperature: Float, - val date: Long + val saveHistory: Boolean, + val historyInterval: Long + ) + + class WriteRequest( + val tx: BleState.TX?, + val saveHistory: Boolean?, + val historyInterval: Long? ) } + class BleState( + val tx: TX + ){ + + enum class TX { + MINUS_40, MINUS_20, MINUS_16, MINUS_12, MINUS_8, MINUS_4, ZERO, PLUS_3, PLUS_4 + } + + } + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt index e7baecc..2699981 100644 --- a/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt +++ b/app/src/main/java/llc/arma/ble/domain/model/BleInfo.kt @@ -1,9 +1,17 @@ package llc.arma.ble.domain.model +import java.util.UUID + class BleInfo( val name: String, val serial: String, - val uuid: String, val batteryLevel: Int, - val rssi: Int -) \ No newline at end of file + val rssi: Int, + val type: Type +){ + + enum class Type(val serviceUUID: String?) { + BEACON(null), THERMOMETER("a77db03a-9bc4-11ed-a8fc-0242ac120002") + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt index 4bc0eb1..9fff913 100644 --- a/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt +++ b/app/src/main/java/llc/arma/ble/domain/repository/BleRepository.kt @@ -12,4 +12,10 @@ interface BleRepository { suspend fun getBleBySerial(serial: String): Result + suspend fun getTemperatureHistoryBySerial(serial: String): List + + suspend fun writeBle(ble: Ble) + + suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest) + } \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/GetTemperatureHistoryBySerial.kt b/app/src/main/java/llc/arma/ble/domain/usecase/GetTemperatureHistoryBySerial.kt new file mode 100644 index 0000000..af8899a --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/usecase/GetTemperatureHistoryBySerial.kt @@ -0,0 +1,17 @@ +package llc.arma.ble.domain.usecase + +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.repository.BleRepository +import javax.inject.Inject + +class GetTemperatureHistoryBySerial @Inject constructor( + private val bleRepository: BleRepository +) { + + suspend operator fun invoke(serial: String): List { + + return bleRepository.getTemperatureHistoryBySerial(serial) + + } + +} \ No newline at end of file diff --git a/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt new file mode 100644 index 0000000..7497509 --- /dev/null +++ b/app/src/main/java/llc/arma/ble/domain/usecase/WriteBle.kt @@ -0,0 +1,20 @@ +package llc.arma.ble.domain.usecase + +import android.app.appsearch.SetSchemaRequest +import llc.arma.ble.domain.model.Ble +import llc.arma.ble.domain.repository.BleRepository +import javax.inject.Inject + +class WriteBle @Inject constructor( + private val bleRepository: BleRepository +) { + + suspend operator fun invoke(ble: Ble){ + bleRepository.writeBle(ble) + } + + suspend operator fun invoke(serial: String, request: Ble.Thermometer.WriteRequest){ + bleRepository.writeBle(serial, request) + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 40909c2..04e3c67 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,9 @@ - + - \ No newline at end of file