Merge branch 'master' of https://github.com/Vineyro/Ble
Conflicts: .idea/vcs.xml
This commit is contained in:
commit
ffba61a55e
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="SERIAL_NUMBER" />
|
||||
<value value="55381e0a" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2023-03-22T08:48:05.794601500Z" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<Ble, BleView> {
|
||||
|
||||
override fun map(input: Ble): BleView {
|
||||
return when(input){
|
||||
is Ble.Beacon -> {
|
||||
BleView.Beacon(
|
||||
info = input.info,
|
||||
state = BleView.BleState(
|
||||
tx = txMapper.map(input.state.tx)
|
||||
)
|
||||
)
|
||||
}
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<BleView, Ble> {
|
||||
|
||||
override fun map(input: BleView): Ble {
|
||||
return when(input){
|
||||
is BleView.Beacon -> {
|
||||
Ble.Beacon(
|
||||
info = input.info,
|
||||
state = Ble.BleState(
|
||||
tx = txMapper.map(input.state.tx)
|
||||
)
|
||||
)
|
||||
}
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package llc.arma.ble.app.ui.mapper
|
||||
|
||||
interface Mapper<I, O> {
|
||||
|
||||
fun map(input: I): O
|
||||
|
||||
fun map(input: List<I>): List<O> = input.map { map(it) }
|
||||
|
||||
}
|
||||
|
|
@ -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<Ble.BleState.TX, BleView.BleState.TX> {
|
||||
|
||||
override fun map(input: Ble.BleState.TX): BleView.BleState.TX {
|
||||
return when(input){
|
||||
Ble.BleState.TX.MINUS_40 -> BleView.BleState.TX.MINUS_40
|
||||
Ble.BleState.TX.MINUS_20 -> BleView.BleState.TX.MINUS_20
|
||||
Ble.BleState.TX.MINUS_16 -> BleView.BleState.TX.MINUS_16
|
||||
Ble.BleState.TX.MINUS_12 -> BleView.BleState.TX.MINUS_12
|
||||
Ble.BleState.TX.MINUS_8 -> BleView.BleState.TX.MINUS_8
|
||||
Ble.BleState.TX.MINUS_4 -> BleView.BleState.TX.MINUS_4
|
||||
Ble.BleState.TX.ZERO -> BleView.BleState.TX.ZERO
|
||||
Ble.BleState.TX.PLUS_3 -> BleView.BleState.TX.PLUS_3
|
||||
Ble.BleState.TX.PLUS_4 -> BleView.BleState.TX.PLUS_4
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<BleView.BleState.TX, Ble.BleState.TX> {
|
||||
|
||||
override fun map(input: BleView.BleState.TX): Ble.BleState.TX {
|
||||
return when(input){
|
||||
BleView.BleState.TX.MINUS_40 -> Ble.BleState.TX.MINUS_40
|
||||
BleView.BleState.TX.MINUS_20 -> Ble.BleState.TX.MINUS_20
|
||||
BleView.BleState.TX.MINUS_16 -> Ble.BleState.TX.MINUS_16
|
||||
BleView.BleState.TX.MINUS_12 -> Ble.BleState.TX.MINUS_12
|
||||
BleView.BleState.TX.MINUS_8 -> Ble.BleState.TX.MINUS_8
|
||||
BleView.BleState.TX.MINUS_4 -> Ble.BleState.TX.MINUS_4
|
||||
BleView.BleState.TX.ZERO -> Ble.BleState.TX.ZERO
|
||||
BleView.BleState.TX.PLUS_3 -> Ble.BleState.TX.PLUS_3
|
||||
BleView.BleState.TX.PLUS_4 -> Ble.BleState.TX.PLUS_4
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,11 +89,78 @@ private fun LoadingState(){
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun DisplayState(){
|
||||
private fun DisplayState(ble: Ble.Beacon){
|
||||
|
||||
Column {
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
content = {
|
||||
|
||||
item {
|
||||
|
||||
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,
|
||||
|
|
@ -99,4 +182,6 @@ private fun DisplayState(){
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
|
||||
|
||||
init {
|
||||
|
||||
savedStateHandle.get<String>("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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() + " %"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<ConnectionViewModel>()
|
||||
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()){
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<ConnectionContract.State, ConnectionContract.Event, ConnectionContract.Effect>() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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(
|
||||
ble: Ble.Thermometer,
|
||||
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
|
||||
) {
|
||||
|
||||
var sheetPage by remember {
|
||||
mutableStateOf<SheetPage?>(null)
|
||||
}
|
||||
|
||||
val viewModel = hiltViewModel<ThermometerViewModel>()
|
||||
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))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ThermometerScreen() {
|
||||
}
|
||||
|
|
@ -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<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() {
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Сохранить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Применить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 = "Применить"
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<TemperatureHistoryViewModel>()
|
||||
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<AxisPosition.Horizontal.Bottom> { 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<Ble.Thermometer.MeasurePoint>
|
||||
) : State()
|
||||
|
||||
}
|
||||
|
||||
sealed class Effect : ViewSideEffect {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@HiltViewModel
|
||||
class TemperatureHistoryViewModel @Inject constructor(
|
||||
private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
|
||||
) : BaseViewModel<TemperatureHistoryContract.State, TemperatureHistoryContract.Event, TemperatureHistoryContract.Effect>() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String, ScanResult>()
|
||||
|
||||
override fun getBleAroundFlow(): Flow<List<BleInfo>> {
|
||||
|
||||
val resultList = mutableMapOf<String, BleInfo>()
|
||||
|
|
@ -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,52 +137,11 @@ class BleRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override suspend fun getBleBySerial(serial: String): Result<Ble, GetBleBySerial.GetBleException> = suspendCancellableCoroutine {
|
||||
override suspend fun getBleBySerial(
|
||||
serial: String
|
||||
): Result<Ble, GetBleBySerial.GetBleException> = suspendCancellableCoroutine {
|
||||
|
||||
val bluetoothManager = app.getSystemService(BluetoothManager::class.java)
|
||||
val bleScanner = bluetoothManager.adapter.bluetoothLeScanner
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
app,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
|
||||
it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)){
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val connected = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
|
||||
.firstOrNull { device -> device.address == serial }
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
){}
|
||||
|
||||
} else {
|
||||
|
||||
val bleCallback = object : ScanCallback() {
|
||||
|
||||
override fun onScanResult(
|
||||
callbackType: Int,
|
||||
result: ScanResult
|
||||
) {
|
||||
|
||||
super.onScanResult(callbackType, result)
|
||||
deviceCache[serial]?.let { result ->
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
app,
|
||||
|
|
@ -141,19 +149,378 @@ class BleRepositoryImpl @Inject constructor(
|
|||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
|
||||
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
|
||||
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<Ble.Thermometer.MeasurePoint> {
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return emptyList()
|
||||
|
||||
}
|
||||
|
||||
override suspend fun writeBle(ble: Ble) {
|
||||
when(ble){
|
||||
is Ble.Beacon -> writeBeacon(ble)
|
||||
is Ble.Thermometer -> writeThermometer(ble)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeBle(
|
||||
serial: String,
|
||||
request: Ble.Thermometer.WriteRequest
|
||||
) {
|
||||
|
||||
deviceCache[serial]?.let { result ->
|
||||
|
||||
request.tx?.let { writeTx(result.device, it) }
|
||||
|
||||
request.historyInterval?.let { writeSaveInterval(result.device, it) }
|
||||
|
||||
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<Byte>(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<Byte>(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()
|
||||
}
|
||||
|
||||
} 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 +528,105 @@ 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
val rssi: Int,
|
||||
val type: Type
|
||||
){
|
||||
|
||||
enum class Type(val serviceUUID: String?) {
|
||||
BEACON(null), THERMOMETER("a77db03a-9bc4-11ed-a8fc-0242ac120002")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -12,4 +12,10 @@ interface BleRepository {
|
|||
|
||||
suspend fun getBleBySerial(serial: String): Result<Ble, GetBleBySerial.GetBleException>
|
||||
|
||||
suspend fun getTemperatureHistoryBySerial(serial: String): List<Ble.Thermometer.MeasurePoint>
|
||||
|
||||
suspend fun writeBle(ble: Ble)
|
||||
|
||||
suspend fun writeBle(serial: String, request: Ble.Thermometer.WriteRequest)
|
||||
|
||||
}
|
||||
|
|
@ -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<Ble.Thermometer.MeasurePoint> {
|
||||
|
||||
return bleRepository.getTemperatureHistoryBySerial(serial)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Ble" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.Ble" parent="android:Theme.Material.Light.NoActionBar" >
|
||||
<item name="android:statusBarColor">#00000000</item>
|
||||
<item name="android:navigationBarColor">#00ffffff</item>
|
||||
<item name="android:windowLightStatusBar" >true</item>
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue