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