Conflicts:
	.idea/vcs.xml
This commit is contained in:
Vineyro 2023-03-24 17:26:46 +07:00
commit ffba61a55e
35 changed files with 2933 additions and 165 deletions

View File

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

View File

@ -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"
}

View File

@ -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" />

View File

@ -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()

View File

@ -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
)
)
}
}
}
}

View File

@ -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
)
)
}
}
}
}

View File

@ -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) }
}

View File

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

View File

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

View File

@ -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)
}
}
}

View File

@ -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
)
}
}
}

View File

@ -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()

View File

@ -1,21 +1,33 @@
package llc.arma.ble.app.ui.screen.beacon
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BeaconScreen(
ble: Ble.Beacon,
onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit
) {
@ -30,6 +42,10 @@ fun BeaconScreen(
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
}
Column {
CenterAlignedTopAppBar(
@ -47,12 +63,12 @@ fun BeaconScreen(
)
},
title = {
if (state is BeaconContract.State.Display) Text(text = state.beacon.info.name)
}
)
when(state){
is BeaconContract.State.Display -> DisplayState()
is BeaconContract.State.Display -> DisplayState(state.beacon)
is BeaconContract.State.Loading -> LoadingState()
}
@ -73,27 +89,96 @@ private fun LoadingState(){
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayState(){
private fun DisplayState(ble: Ble.Beacon){
Surface(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
Column {
}
) {
LazyColumn(
modifier = Modifier.weight(1f),
content = {
Box(modifier = Modifier.fillMaxSize()) {
item {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(bleInfo = ble.info)
}
}
item {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "-40 db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
}
)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Сохранить"
)
}
}

View File

@ -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
)
}
}
}

View File

@ -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() + " %"
)
}
}
}
}

View File

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

View File

@ -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()

View File

@ -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()){
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
Column {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
}
@Composable
private fun DisplayException(exception: GetBleBySerial.GetBleException){
private fun DisplayException(
exception: GetBleBySerial.GetBleException
){
Column {
}
}

View File

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

View File

@ -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()
}
)
}
)
}

View File

@ -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()
}
}
}

View File

@ -1,7 +1,499 @@
package llc.arma.ble.app.ui.screen.thermometer
import androidx.compose.runtime.Composable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.screen.thermometer.view.*
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
INTERVAL, POWER, TEMPERATURE_HISTORY
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThermometerScreen() {
fun ThermometerScreen(
ble: Ble.Thermometer,
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
) {
var sheetPage by remember {
mutableStateOf<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))
}
}
)
}
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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 = "Сохранить"
)
}
}
}
}

View File

@ -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 = "Применить"
)
}
}
}
}

View File

@ -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))
}
}

View File

@ -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 = "Применить"
)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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,72 +137,392 @@ 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
deviceCache[serial]?.let { result ->
if (ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
if (ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)){
if (it.isActive) {
val info = result.info
val state = Ble.BleState(
tx = when (result.scanRecord?.txPowerLevel) {
-40 -> Ble.BleState.TX.MINUS_40
-20 -> Ble.BleState.TX.MINUS_20
-16 -> Ble.BleState.TX.MINUS_16
-12 -> Ble.BleState.TX.MINUS_12
-8 -> Ble.BleState.TX.MINUS_8
-4 -> Ble.BleState.TX.MINUS_4
3 -> Ble.BleState.TX.PLUS_3
4 -> Ble.BleState.TX.PLUS_4
else -> Ble.BleState.TX.ZERO
}
)
CoroutineScope(Dispatchers.IO).launch {
val resultValue = when (info.type) {
BleInfo.Type.BEACON -> Ble.Beacon(
info = info,
state = state
)
BleInfo.Type.THERMOMETER -> {
Ble.Thermometer(
info = info,
state = state,
thermometerState = readThermometerState(result)
)
}
}
it.resume(Result.success(resultValue)) {}
}
}
} else {
it.resume(Result.failure(GetBleBySerial.GetBleException.BlePermissionDenied)) {}
}
}
}
private suspend fun readThermometerState(
record: ScanResult
): Ble.Thermometer.ThermometerState {
return Ble.Thermometer.ThermometerState(
temperature = readTemperature(record),
saveHistory = record.timerEnabled,
historyInterval = readHistoryInterval(record)
)
}
private suspend fun readTemperature(
record: ScanResult
): Float {
val dataResult = readCharacteristic(
device = record.device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
)
return (dataResult[0] + dataResult[1] * 256).toFloat() / 100f
}
private suspend fun readHistoryInterval(
record: ScanResult
): Long {
writeCharacteristic(
device = record.device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb"),
writeData = byteArrayOf(3, 0, 0, 0, 0)
)
val dataResult = readCharacteristic(
device = record.device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b2d8-0000-1000-8000-00805f9b34fb")
)
return if(dataResult.size == 4){
dataResult.getUIntAt(0).toLong()
}else{
0
}
}
override suspend fun getTemperatureHistoryBySerial(
serial: String
): List<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
)
}
}
}
val connected = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
.firstOrNull { device -> device.address == serial }
return emptyList()
if(connected != null){
}
it.resume(
Result.success(
Ble.Beacon(
info = BleInfo(
name = connected.name,
serial = connected.address,
uuid = connected.uuids?.firstOrNull()?.toString() ?: "",
rssi = 0,
batteryLevel = 1
)
)
)
){}
override suspend fun writeBle(ble: Ble) {
when(ble){
is Ble.Beacon -> writeBeacon(ble)
is Ble.Thermometer -> writeThermometer(ble)
}
}
} else {
override suspend fun writeBle(
serial: String,
request: Ble.Thermometer.WriteRequest
) {
val bleCallback = object : ScanCallback() {
deviceCache[serial]?.let { result ->
override fun onScanResult(
callbackType: Int,
result: ScanResult
) {
request.tx?.let { writeTx(result.device, it) }
super.onScanResult(callbackType, result)
request.historyInterval?.let { writeSaveInterval(result.device, it) }
if (ActivityCompat.checkSelfPermission(
request.saveHistory?.let { writeSaveEnabled(result.device, it) }
}
}
private suspend fun writeBeacon(ble: Ble.Beacon){
deviceCache[ble.info.serial]?.device?.let {
writeTx(it, ble.state.tx)
}
}
private suspend fun writeThermometer(ble: Ble.Thermometer){
deviceCache[ble.info.serial]?.device?.let {
writeTx(it, ble.state.tx)
writeSaveInterval(it, ble.thermometerState.historyInterval)
}
}
private suspend fun writeTx(
device: BluetoothDevice,
tx: Ble.BleState.TX
) {
writeCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("00002a07-0000-1000-8000-00805f9b34fb"),
writeData = byteArrayOf(
when(tx) {
Ble.BleState.TX.MINUS_40 -> -40
Ble.BleState.TX.MINUS_20 -> -20
Ble.BleState.TX.MINUS_16 -> -16
Ble.BleState.TX.MINUS_12 -> -12
Ble.BleState.TX.MINUS_8 -> -8
Ble.BleState.TX.MINUS_4 -> -4
Ble.BleState.TX.ZERO -> 0
Ble.BleState.TX.PLUS_3 -> 3
Ble.BleState.TX.PLUS_4 -> 4
}
)
)
}
private suspend fun writeSaveInterval(
device: BluetoothDevice,
interval: Long
) {
fun UInt.to4ByteArrayInBigEndian(): ByteArray =
(3 downTo 0).map {
(this shr (it * Byte.SIZE_BITS)).toByte()
}.reversed().toByteArray()
writeCharacteristic(
device = device,
serviceId = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002"),
characteristicId = UUID.fromString("0000b6f2-0000-1000-8000-00805f9b34fb"),
writeData = mutableListOf<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()
}
it.resume(
Result.success(
Ble.Beacon(
info = BleInfo(
name = result.device.name,
serial = result.device.address,
uuid = result.device.uuids?.firstOrNull()?.toString() ?: "",
rssi = result.rssi,
batteryLevel = 1
)
)
)
){}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt?,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt?.services?.firstOrNull { service ->
service.uuid == serviceId
}?.characteristics?.firstOrNull { characteristic ->
characteristic.uuid == characteristicId
}?.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt.readCharacteristic(it)
}
}
@ -161,19 +530,103 @@ class BleRepositoryImpl @Inject constructor(
}
bleScanner.startScan(
listOf(ScanFilter.Builder().setDeviceAddress(serial).build()),
ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
.setReportDelay(0L)
.build(),
bleCallback
)
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
it.resume(value)
}
}
device.connectGatt(app, true, callback)
}
private suspend fun writeCharacteristic(
device: BluetoothDevice,
serviceId: UUID,
characteristicId: UUID,
writeData: ByteArray
) = suspendCoroutine {
val callback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt?,
status: Int,
newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
gatt?.discoverServices()
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt?,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt?.services?.firstOrNull { service ->
service.uuid == serviceId
}?.characteristics?.firstOrNull { characteristic ->
characteristic.uuid == characteristicId
}?.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || ActivityCompat.checkSelfPermission(
app,
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeCharacteristic(it, writeData, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
it.value = writeData
gatt.writeCharacteristic(it)
}
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic?,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
it.resume(Unit)
}
}
device.connectGatt(app, true, callback)
}
}

View File

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

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

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