Merge remote-tracking branch 'origin/master'

This commit is contained in:
Vineyro 2023-11-12 19:02:12 +07:00
commit cccc65153d
67 changed files with 7618 additions and 561 deletions

View File

@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -11,10 +11,10 @@ android {
defaultConfig {
applicationId "llc.arma.ble"
minSdk 24
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
versionCode 12
versionName "1.2.12"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -46,6 +46,13 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
applicationVariants.configureEach { variant ->
variant.outputs.configureEach {
outputFileName = "Arma BLE v${defaultConfig.versionName}.apk"
}
}
}
dependencies {
@ -76,8 +83,10 @@ dependencies {
implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta"
implementation "com.patrykandpatrick.vico:core:1.6.6"
implementation "com.patrykandpatrick.vico:compose:1.6.6"
implementation "com.patrykandpatrick.vico:compose-m3:1.6.6"
implementation "com.patrykandpatrick.vico:core:1.7.1"
implementation "com.patrykandpatrick.vico:compose:1.7.1"
implementation "com.patrykandpatrick.vico:compose-m3:1.7.1"
implementation files('libs/poishadow-all.jar')
}

BIN
app/libs/poishadow-all.jar Normal file

Binary file not shown.

View File

@ -18,6 +18,13 @@
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -27,7 +34,20 @@
android:supportsRtl="true"
android:theme="@style/Theme.App.Starting"
tools:targetApi="31"
android:name=".app.framework.App">
android:name=".app.framework.App"
android:requestLegacyExternalStorage="true">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="llc.arma.ble.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<activity
android:name=".app.ui.MainActivity"

View File

@ -5,4 +5,18 @@ import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp()
class App : Application() {
/**
*Без этого apache poi на android не заводится
*@link https://github.com/centic9/poi-on-android
*/
override fun onCreate() {
super.onCreate()
System.setProperty("org.apache.poi.javax.xml.stream.XMLInputFactory", "com.fasterxml.aalto.stax.InputFactoryImpl");
System.setProperty("org.apache.poi.javax.xml.stream.XMLOutputFactory", "com.fasterxml.aalto.stax.OutputFactoryImpl");
System.setProperty("org.apache.poi.javax.xml.stream.XMLEventFactory", "com.fasterxml.aalto.stax.EventFactoryImpl");
}
}

View File

@ -5,7 +5,11 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import llc.arma.ble.data.BleRepositoryImpl
import llc.arma.ble.data.EmailRepositoryImpl
import llc.arma.ble.data.XlsxRepositoryImpl
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.repository.EmailRepository
import llc.arma.ble.domain.repository.XlsxRepository
@Module
@InstallIn(SingletonComponent::class)
@ -14,4 +18,10 @@ interface RepositoryBinding {
@Binds
fun bindBleRepository(bleRepositoryImpl: BleRepositoryImpl): BleRepository
@Binds
fun bindEmailRepository(repository: EmailRepositoryImpl): EmailRepository
@Binds
fun bindXlsxRepository(repository: XlsxRepositoryImpl): XlsxRepository
}

View File

@ -1,8 +1,14 @@
package llc.arma.ble.app.ui
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
@ -16,11 +22,10 @@ import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -34,6 +39,7 @@ import llc.arma.ble.app.ui.theme.BleTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -56,6 +62,10 @@ class MainActivity : ComponentActivity() {
mutableStateOf<@Composable () -> Unit>({})
}
if(modalState.currentValue == ModalBottomSheetValue.Hidden){
sheetContent = {}
}
CompositionLocalProvider(
LocalBottomDialogState provides BottomState(
sheetState = modalState,
@ -125,20 +135,37 @@ class MainActivity : ComponentActivity() {
) {
val multiplePermissionsState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
)
} else {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
}else{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
)
)
} else {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
}
if (multiplePermissionsState.allPermissionsGranted) {

View File

@ -38,7 +38,7 @@ class BottomDialogState @OptIn(ExperimentalMaterialApi::class) constructor(
content: @Composable () -> Unit
){
setContent(content)
if(sheetState?.currentValue != ModalBottomSheetValue.Expanded)
//if(sheetState?.currentValue != ModalBottomSheetValue.Expanded)
sheetState?.show()
}

View File

@ -0,0 +1,50 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun SignalLevel(
modifier: Modifier = Modifier,
maxLevel: Int = 5,
level: Int
){
val step = (16 - 4) / 4
Row(
modifier = modifier.height(16.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.Bottom
) {
for(col in 0..4 step 1){
Surface(
color = LocalContentColor.current.copy(
alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled
),
shape = CircleShape,
modifier = Modifier
.width(4.dp)
.defaultMinSize(minHeight = 4.dp)
.height(((col + 1) * step).dp)
) { }
}
}
}

View File

@ -35,6 +35,13 @@ class BleMapper @Inject constructor(
is Ble.Accelerometer -> {
BleView.Accelerometer(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = BleView.Accelerometer.AccelerometerState(
saveHistory = input.accelerometerState.saveHistory,
historyInterval = input.accelerometerState.historyInterval
)
)
}
}

View File

@ -34,7 +34,14 @@ class BleViewMapper @Inject constructor(
is BleView.Accelerometer -> {
Ble.Accelerometer(
info = input.info
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx)
),
accelerometerState = Ble.Accelerometer.AccelerometerState(
saveHistory = input.accelerometerState.saveHistory,
historyInterval = input.accelerometerState.historyInterval,
)
)
}
}

View File

@ -3,15 +3,31 @@ 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.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
sealed class BleView(
val info: BleInfo
) {
class Accelerometer(
info: BleInfo
) : BleView(info)
info: BleInfo,
val state: BleState,
val accelerometerState: AccelerometerState
) : BleView(info) {
class AccelerometerState(
saveHistory: Ble.Accelerometer.History,
historyInterval: Long,
) {
var saveHistory by mutableStateOf(saveHistory)
var historyInterval by mutableStateOf(historyInterval)
}
}
class Beacon(
info: BleInfo,
@ -58,9 +74,27 @@ sealed class BleView(
MINUS_4(-4),
ZERO(0),
PLUS_3(3),
PLUS_4(4)
PLUS_4(4);
val powerPercentage: Int
get() {
return when(this){
MINUS_40 -> 1
MINUS_20 -> 5
MINUS_16 -> 7
MINUS_12 -> 10
MINUS_8 -> 16
MINUS_4 -> 20
ZERO -> 40
PLUS_3 -> 80
PLUS_4 -> 100
}
}
}
}
}

View File

@ -48,7 +48,7 @@ class BleListContract {
data class Filter(
val name: String = "",
val mac: String = "",
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-30f),
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-10f),
val bleType: BleInfo.Type? = null
)

View File

@ -15,6 +15,7 @@ import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -29,9 +30,11 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -119,7 +122,14 @@ fun BleListScreen(
}
)
if(state.bleList.isEmpty()){
val filteredData = state.bleList.filter {
(it.type == state.filter.bleType || state.filter.bleType == null) &&
it.name.contains(state.filter.name) &&
it.serial.contains(state.filter.mac) &&
state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE)
}
if(filteredData.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
@ -128,37 +138,44 @@ fun BleListScreen(
)
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
if(filteredData.isEmpty()){
items(items = state.connectedBleList){
Box(modifier = Modifier.fillMaxSize()){
Text(
modifier = Modifier.align(Alignment.Center),
style = MaterialTheme.typography.titleMedium,
text = "Метки в области не найдены"
)
}
} else {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
) {
items(items = state.connectedBleList){
ConnectedBleItem(ble = it) {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
}
items(items = filteredData.sortedBy { it.name }.reversed()) {
BleItem(
ble = it,
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
)
ConnectedBleItem(ble = it) {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
}
val filteredData = state.bleList.filter {
(it.type == state.filter.bleType || state.filter.bleType == null) &&
it.name.contains(state.filter.name) &&
it.serial.contains(state.filter.mac) &&
state.filter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE)
}
items(items = filteredData.sortedBy { it.name }.reversed()) {
BleItem(
ble = it,
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
)
}
}
}
@ -184,6 +201,16 @@ private fun ItemIcon(
}
private fun Int.toSignalLevel(): Int {
return when(this){
in -30 downTo -52 -> 4
in -51 downTo -63 -> 3
in -62 downTo -75 -> 2
in -74 downTo -89 -> 1
else -> 0
}
}
@Composable
private fun BleItem(
ble: BleInfo,
@ -199,28 +226,27 @@ private fun BleItem(
val highAlpha = ContentAlpha.high
val disabledAlpha = ContentAlpha.disabled
var alpha by remember {
mutableStateOf(
if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){
disabledAlpha
} else {
highAlpha
}
var time by remember {
mutableLongStateOf(
SystemClock.elapsedRealtime()
)
}
LaunchedEffect(ble.scanTime) {
while(true) {
alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){
disabledAlpha
} else {
highAlpha
}
delay(800)
time = SystemClock.elapsedRealtime()
delay(100)
}
}
var alpha = if(SystemClock.elapsedRealtime() - ble.scanTime > 10_000){
disabledAlpha
} else {
highAlpha
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
@ -234,16 +260,40 @@ private fun BleItem(
) {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = when(ble.type){
BleInfo.Type.BEACON -> Icons.Rounded.Nfc
BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
},
contentDescription = null
)
Box {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = when (ble.type) {
BleInfo.Type.BEACON -> Icons.Rounded.Nfc
BleInfo.Type.THERMOMETER -> Icons.Rounded.Thermostat
BleInfo.Type.ACCELEROMETER -> Icons.Rounded.Speed
},
contentDescription = null
)
}
if(ble.recordEnabled){
Surface(
shape = CircleShape,
color = color,
modifier = Modifier.align(Alignment.TopEnd)
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.size(12.dp).padding(2.dp)
) {
}
}
}
}
Column {
@ -255,6 +305,26 @@ private fun BleItem(
text = ble.serial
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
Text(
style = MaterialTheme.typography.bodyMedium,
text = String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
@ -264,11 +334,9 @@ private fun BleItem(
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.NetworkCell,
contentDescription = null
)
SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
Spacer(modifier = Modifier.width(4.dp))
Box {
@ -329,17 +397,10 @@ private fun BleItem(
modifier = Modifier.alpha(0.7f)
) {
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
}
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.ArrowRightAlt,
contentDescription = null,
tint = color
contentDescription = null
)
Box {
@ -356,7 +417,7 @@ private fun BleItem(
modifier = Modifier.alpha(0f)
)
val lastAdv = ((SystemClock.elapsedRealtime() - ble.scanTime) / 1_000)
val lastAdv = ((time - ble.scanTime) / 1_000)
Text(
style = MaterialTheme.typography.bodyMedium,

View File

@ -7,14 +7,17 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.GetBleAroundFlow
import llc.arma.ble.domain.usecase.GetConnectedBleDevices
import llc.arma.ble.domain.usecase.MeasureData
import javax.inject.Inject
@HiltViewModel
class BleListViewModel @Inject constructor(
getBleAroundFlow: GetBleAroundFlow,
getConnectedBleDevices: GetConnectedBleDevices
getConnectedBleDevices: GetConnectedBleDevices,
exportToXlsx: ExportToXlsx
) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() {
init {

View File

@ -244,8 +244,8 @@ fun Filter(
onValueChange = {
onEvent(BleListContract.Event.OnRssiRangeChanged(it))
},
valueRange = (-100f)..(-30f),
steps = 69,
valueRange = (-100f)..(-10f),
steps = 89,
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.primary,
inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)

View File

@ -4,6 +4,7 @@ import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.common.BleException
@ -26,6 +27,11 @@ class ConnectionContract {
val event: ThermometerContract.Effect.Navigation
) : Event()
data class OnAccelNavigationEvent(
val event: AccelerometerContract.Effect.Navigation
) : Event()
}
sealed class State : ViewState {

View File

@ -20,6 +20,7 @@ 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.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerScreen
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconScreen
import llc.arma.ble.app.ui.screen.password.ChangePasswordContract
@ -117,7 +118,11 @@ fun ConnectionScreen(
}
is Ble.Accelerometer -> {
AccelerometerScreen(ble = state.ble)
AccelerometerScreen(ble = state.ble) {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
}
}
}

View File

@ -10,6 +10,7 @@ import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
@ -35,6 +36,7 @@ class ConnectionViewModel @Inject constructor(
is ConnectionContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ConnectionContract.Event.OnThermometerNavigationEvent -> reduce(viewState.value, event)
is ConnectionContract.Event.RefreshBle -> reduce(viewState.value, event)
is ConnectionContract.Event.OnAccelNavigationEvent -> reduce(viewState.value, event)
}
}
@ -74,6 +76,19 @@ class ConnectionViewModel @Inject constructor(
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
when(event.event){
AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
}
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnNavigateUp

View File

@ -4,21 +4,102 @@ import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerContract {
sealed class Event : ViewEvent {
object OnShowAccelerometerMeasure : Event()
object OnShowAccelerometerAccel : Event()
object OnHideAccelerometerAccel : Event()
object OnHideAccelerometerMeasure : Event()
object OnShowAccelerometerSpectre : Event()
object OnHideAccelerometerSpectre : Event()
object OnShowAccelerometerHistory : Event()
object OnHideAccelerometerHistory : Event()
data class OnAccelViewModeEdit(
val next: Next
) : Event()
enum class Next {
ACCEL, SPECTRE, HISTORY
}
data class OnAccelScaleEdit(
val next: Next
) : Event()
object OnAccelEdit : Event()
object OnSpectreEdit : Event()
object OnHistoryEdit : Event()
object OnFftFrequencyEdit : Event()
object OnFftAxisEdit : Event()
object OnFftModeEdit : Event()
object OnPowerEdit : Event()
object OnShowWriteBlePreview : Event()
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
object OnChangePassword : Event()
object OnSaveIntervalEdit : Event()
object OnHideHistoryEdit : Event()
data class OnBleChanged(
val ble: Ble.Accelerometer,
): Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
data class OnAccelViewModelChanged(
val mode: AccelViewMode
) : Event()
data class OnHistoryViewModeChanged(
val mode: AccelViewMode
) : Event()
data class OnFftFrequencyChanged(
val frequency: FftFrequency
) : Event()
data class OnFftAxisChanged(
val axis: FftAxis
) : Event()
data class OnFftModeChanged(
val mode: FftViewMode
) : Event()
data class OnAccelScaleChanged(
val scale: AccelScale
) : Event()
data class OnHistoryScaleChanged(
val scale: AccelScale
) : Event()
data class OnSaveHistoryChanged(
val save: Boolean
) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
}
sealed class State : ViewState {
@ -28,13 +109,73 @@ class AccelerometerContract {
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer,
) : State()
val writeState: WriteState?,
val accelViewMode: AccelViewMode,
val accelScale: AccelScale,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
object Success : WriteState()
object Failure : WriteState()
}
}
}
sealed class Effect : ViewSideEffect {
object ShowAccelerometerMeasure : Effect()
object ShowAccelerometerAccel : Effect()
object ShowAccelerometerSpectre : Effect()
object ShowAccelerometerHistory : Effect()
object ShowPowerPicker : Effect()
object HidePowerPicker : Effect()
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
object HideHistoryEdit : Effect()
data class ShowAccelViewEdit(
val next: Event.Next
) : Effect()
data class ShowAccelScaleEdit(
val next: Event.Next
) : Effect()
object ShowAccelEdit : Effect()
object ShowSpectreEdit : Effect()
object ShowFftFrequencyEdit : Effect()
object ShowFftAxisEdit : Effect()
object ShowFftModeEdit : Effect()
object HideIntervalPicker : Effect()
object ShowIntervalPicker : Effect()
object ShowHistoryEdit : Effect()
sealed class Navigation : Effect() {
object NavigateToChangePassword : Navigation()
}
}

View File

@ -4,9 +4,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel
@ -15,19 +17,33 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerMeasure
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftAxisEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftModeEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFrequencyEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelScaleEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelSpectreEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelViewEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerSpectre
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerAccel
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerHistory
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.HistoryEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.IntervalEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.PowerEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.Write
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
MEASURE_HISTORY
HISTORY, ACCEL_SCALE, SPECTRE_SCALE, HISTORY_MODE_EDIT, HISTORY_SCALE, HISTORY_EDIT, ACCEL_EDIT, ACCEL, POWER, WRITE, SPECTRE, ACCEL_MODE_EDIT, SPECTRE_MODE_EDIT, SPECTRE_EDIT, FREQUENCY_EDIT, AXIS_EDIT, FFT_MODE_EDIT, INTERVAL_EDIT
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AccelerometerScreen(
ble: Ble.Accelerometer,
onEvent: (AccelerometerContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
@ -43,30 +59,402 @@ fun AccelerometerScreen(
mutableStateOf<SheetPage?>(null)
}
LaunchedEffect(
key1 = bottomDialog.sheetState?.currentValue,
block = {
if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
bottomDialog.setContent({})
sheetPage = null
}
}
)
val scope = rememberCoroutineScope()
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.MEASURE_HISTORY -> launch {
SheetPage.HISTORY -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerMeasure(ble = currentState.accelerometer.info)
AccelerometerHistory(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
fftMode = currentState.fftViewMode,
frequency = currentState.fftFrequency,
accelScale = currentState.accelScale
)
}
}
}
SheetPage.ACCEL -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerAccel(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
fftMode = currentState.fftViewMode,
frequency = currentState.fftFrequency,
accelScale = currentState.accelScale
)
}
}
}
SheetPage.SPECTRE -> launch {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
bottomDialog.show {
AccelerometerSpectre(
ble = currentState.accelerometer.info,
accelMode = currentState.accelViewMode,
fftAxis = currentState.fftAxis,
fftMode = currentState.fftViewMode,
frequency = currentState.fftFrequency,
accelScale = currentState.accelScale
)
}
}
}
SheetPage.POWER -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
PowerEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
}
SheetPage.ACCEL_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.SPECTRE_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.SPECTRE,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.SPECTRE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelSpectreEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.FREQUENCY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFrequencyEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.AXIS_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftAxisEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.FFT_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftModeEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
IntervalEdit(
state = currentState.accelerometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.ACCEL_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.SPECTRE_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.SPECTRE,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.ACCEL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.HISTORY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
HistoryEdit(
state = currentState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
null -> {
bottomDialog.hide()
}
}
}
DisposableEffect(key1 = Unit, effect = {
onDispose {
scope.launch {
bottomDialog.hide()
}
}
})
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
AccelerometerContract.Effect.ShowAccelerometerMeasure -> {
is AccelerometerContract.Effect.ShowAccelerometerAccel -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.ACCEL
}
is AccelerometerContract.Effect.HidePowerPicker -> launch {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER
}
is AccelerometerContract.Effect.HideWriteBle -> launch {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowWriteBle -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.ShowAccelerometerSpectre -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.SPECTRE
}
is AccelerometerContract.Effect.ShowAccelViewEdit -> launch {
sheetPage = null
delay(100)
sheetPage = when(it.next){
AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_MODE_EDIT
AccelerometerContract.Event.Next.SPECTRE -> SheetPage.SPECTRE_MODE_EDIT
AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_MODE_EDIT
}
}
is AccelerometerContract.Effect.ShowSpectreEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.SPECTRE_EDIT
}
is AccelerometerContract.Effect.ShowFftFrequencyEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.FREQUENCY_EDIT
}
is AccelerometerContract.Effect.ShowFftAxisEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.AXIS_EDIT
}
is AccelerometerContract.Effect.ShowFftModeEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.FFT_MODE_EDIT
}
is AccelerometerContract.Effect.ShowAccelerometerHistory -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY
}
is AccelerometerContract.Effect.HideIntervalPicker -> {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowIntervalPicker -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL_EDIT
}
is AccelerometerContract.Effect.ShowAccelScaleEdit -> {
sheetPage = null
delay(100)
sheetPage = when(it.next){
AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_SCALE
AccelerometerContract.Event.Next.SPECTRE -> SheetPage.SPECTRE_SCALE
AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_SCALE
}
}
is AccelerometerContract.Effect.ShowAccelEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.ACCEL_EDIT
}
is AccelerometerContract.Effect.Navigation -> {
onEvent(it)
}
is AccelerometerContract.Effect.ShowHistoryEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY_EDIT
}
is AccelerometerContract.Effect.HideHistoryEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.MEASURE_HISTORY
}
}
}.launchIn(this)

View File

@ -1,16 +1,26 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
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.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class AccelerometerViewModel @Inject constructor(
private val bleMapper: BleMapper
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
override fun setInitialState() = AccelerometerContract.State.Loading
@ -18,29 +28,475 @@ class AccelerometerViewModel @Inject constructor(
override fun handleEvents(event: AccelerometerContract.Event) {
when(event){
is AccelerometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerMeasure -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerMeasure -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerAccel -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerAccel -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerSpectre -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerSpectre -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelViewModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelViewModelChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSpectreEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftFrequencyEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftAxisChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftFrequencyChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftAxisEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelScaleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelScaleEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryScaleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryViewModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideHistoryEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerMeasure
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerMeasure
event: AccelerometerContract.Event.OnHideHistoryEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerMeasure
AccelerometerContract.Effect.HideHistoryEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryViewModeChanged
) {
if(state is AccelerometerContract.State.Display){
var saveHistory = state.accelerometer.accelerometerState.saveHistory
if(saveHistory is Ble.Accelerometer.History.Enabled){
saveHistory = Ble.Accelerometer.History.Enabled(
mode = event.mode,
scale = saveHistory.scale,
detailed = saveHistory.detailed
)
}
state.accelerometer.accelerometerState.saveHistory = saveHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryScaleChanged
) {
if(state is AccelerometerContract.State.Display){
var saveHistory = state.accelerometer.accelerometerState.saveHistory
if(saveHistory is Ble.Accelerometer.History.Enabled){
saveHistory = saveHistory.copy(scale = event.scale)
}
state.accelerometer.accelerometerState.saveHistory = saveHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryEdit
) {
setEffect {
AccelerometerContract.Effect.ShowHistoryEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelScaleChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
accelScale = event.scale
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelScaleEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelScaleEdit(
event.next
)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalEdit
) {
setEffect {
AccelerometerContract.Effect.ShowIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.historyInterval = event.interval
}
setEffect {
AccelerometerContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnChangePassword
) {
setEffect {
AccelerometerContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveHistoryChanged
) {
if(state is AccelerometerContract.State.Display) {
if(event.save){
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.History.Enabled(
scale = AccelScale.S_2,
mode = AccelViewMode.ACCELERATION,
detailed = true
)
setEffect {
AccelerometerContract.Effect.ShowHistoryEdit
}
} else {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.History.Disabled
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftModeEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftModeEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftAxisEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftAxisEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftAxisChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftAxis = event.axis
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftModeChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftViewMode = event.mode
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftFrequencyChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftFrequency = event.frequency
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftFrequencyEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftFrequencyEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSpectreEdit
) {
setEffect {
AccelerometerContract.Effect.ShowSpectreEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerSpectre
) {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerSpectre
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerSpectre
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerHistory
) {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerHistory
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
writeState = null
)
}
}
setEffect {
AccelerometerContract.Effect.HideWriteBle
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
saveHistory = if(newBle.accelerometerState.saveHistory == state.origin.accelerometerState.saveHistory) null else newBle.accelerometerState.saveHistory,
historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
)
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
AccelerometerContract.Effect.ShowWriteBle
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.state.tx = event.tx
}
setEffect {
AccelerometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelViewModelChanged
) {
if(state is AccelerometerContract.State.Display) {
setState {
state.copy(
accelViewMode = event.mode
)
}
}
setEffect {
AccelerometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerEdit
) {
setEffect {
AccelerometerContract.Effect.ShowPowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelViewModeEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelViewEdit(event.next)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerAccel
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerAccel
) {
viewModelScope.launch {
setEffect {
AccelerometerContract.Effect.ShowAccelerometerAccel
}
}
}
private fun reduce(
@ -52,7 +508,9 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.State.Display -> setState {
state.copy(
origin = Ble.Accelerometer(
info = event.ble.info
info = event.ble.info,
state = event.ble.state,
accelerometerState = state.origin.accelerometerState
)
)
}
@ -60,11 +518,85 @@ class AccelerometerViewModel @Inject constructor(
is AccelerometerContract.State.Loading -> setState {
AccelerometerContract.State.Display(
origin = event.ble,
accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer
accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer,
writeState = null,
accelViewMode = AccelViewMode.ACCELERATION,
fftAxis = FftAxis.AUTO,
fftFrequency = FftFrequency.F_400,
fftViewMode = FftViewMode.SPECTRE,
accelScale = AccelScale.S_2
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
state.writeState?.let { request ->
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
}
writeBle(state.accelerometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistory = request.writeRequest.saveHistory
?: currentState.origin.accelerometerState.saveHistory
)
)
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}

View File

@ -0,0 +1,172 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val accelMode = state.accelViewMode
val fftMode = state.fftViewMode
val fftAxis = state.fftAxis
val fftFrequency = state.fftFrequency
val accelScale = state.accelScale
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Ускорение",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(
AccelerometerContract.Event.OnAccelViewModeEdit(
next = AccelerometerContract.Event.Next.ACCEL
)
)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel view mode"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelMode.localized
)
}
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(AccelerometerContract.Event.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.ACCEL))
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel scale"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelScale.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowAccelerometerAccel)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Продолжить"
)
}
}
}
}

View File

@ -0,0 +1,100 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelFftAxisEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftAxis = state.fftAxis
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft axis",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftAxis.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftAxisChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftAxis,
onClick = {
onEvent(AccelerometerContract.Event.OnFftAxisChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnSpectreEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,100 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelFftModeEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftMode = state.fftViewMode
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftViewMode.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftModeChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftMode,
onClick = {
onEvent(AccelerometerContract.Event.OnFftModeChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnSpectreEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,110 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
/*val AccelViewMode.localized: String
get() {
return when(this){
ACCELERATION -> "Ускорение"
PEAK_ACCELERATION -> "Пиковое ускорение"
RMS -> "Среднеквадратичное ускорение"
ANGLE -> "Угол"
}
}*/
@Composable
fun AccelFrequencyEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftFrequency = state.fftFrequency
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft frequency",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftFrequency.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it)) }
.padding(4.dp)
) {
RadioButton(
selected = it == fftFrequency,
onClick = {
onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it))
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnSpectreEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,138 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelScaleEdit(
next: AccelerometerContract.Event.Next,
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var fftMode = when(next){
AccelerometerContract.Event.Next.ACCEL,
AccelerometerContract.Event.Next.SPECTRE ->
state.accelScale
AccelerometerContract.Event.Next.HISTORY -> {
val history = state.accelerometer.accelerometerState.saveHistory
if (history is Ble.Accelerometer.History.Enabled)
history.scale
else {
state.accelScale
}
}
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel scale",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
AccelScale.values().forEach {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable {
when(next){
AccelerometerContract.Event.Next.ACCEL,
AccelerometerContract.Event.Next.SPECTRE ->
onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
}
}
.padding(4.dp)
) {
RadioButton(
selected = it == fftMode,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL,
AccelerometerContract.Event.Next.SPECTRE ->
onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
}
}
)
Text(text = it.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelEdit)
AccelerometerContract.Event.Next.SPECTRE ->
onEvent(AccelerometerContract.Event.OnSpectreEdit)
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,294 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
val FftFrequency.localized: String
get() {
return when(this){
FftFrequency.OFF -> "откл"
FftFrequency.F_1 -> "1 Гц"
FftFrequency.F_10 -> "10 Гц"
FftFrequency.F_25 -> "25 Гц"
FftFrequency.F_50 -> "50 Гц"
FftFrequency.F_100 -> "100 Гц"
FftFrequency.F_200 -> "200 Гц"
FftFrequency.F_400 -> "400 Гц"
FftFrequency.F_1620 -> "1620 Гц"
FftFrequency.F_1344 -> "1344 Гц"
}
}
val FftAxis.localized: String
get() {
return when(this){
FftAxis.AUTO -> "Авто"
FftAxis.X -> "Ось X"
FftAxis.Y -> "Ось Y"
FftAxis.Z -> "Ось Z"
}
}
val FftViewMode.localized: String
get() {
return when(this){
FftViewMode.SPECTRE -> "Спектр"
FftViewMode.X -> "Ось X"
FftViewMode.Y -> "Ось Y"
FftViewMode.Z -> "Ось Z"
}
}
val AccelScale.localized: String
get() {
return when(this){
AccelScale.S_2 -> "2g"
AccelScale.S_4 -> "4g"
AccelScale.S_8 -> "8g"
AccelScale.S_16 -> "16g"
}
}
@Composable
fun AccelSpectreEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val accelMode = state.accelViewMode
val fftMode = state.fftViewMode
val fftAxis = state.fftAxis
val fftFrequency = state.fftFrequency
val accelScale = state.accelScale
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Спектр",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnFftModeEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Fft view mode"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftMode.localized
)
}
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(AccelerometerContract.Event.OnFftAxisEdit) }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Fft axis"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftAxis.localized
)
}
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(AccelerometerContract.Event.OnFftFrequencyEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Fft frequency"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftFrequency.localized
)
}
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(AccelerometerContract.Event.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.SPECTRE))
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel scale"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelScale.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowAccelerometerSpectre)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Продолжить"
)
}
}
}
}

View File

@ -0,0 +1,124 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.AccelViewMode.*
val AccelViewMode.localized: String
get() {
return when(this){
ACCELERATION -> "Ускорение"
PEAK_ACCELERATION -> "Пиковое ускорение"
RMS -> "Среднеквадратичное ускорение"
VIBRATION -> "Вибрация"
ANGLE -> "Угол"
}
}
@Composable
fun AccelViewEdit(
next: AccelerometerContract.Event.Next,
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelViewMode) {
mutableStateOf(state.accelViewMode)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
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.localized)
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
when(next){
AccelerometerContract.Event.Next.ACCEL -> {
onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
AccelerometerContract.Event.Next.SPECTRE -> {
onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
onEvent(AccelerometerContract.Event.OnSpectreEdit)
}
AccelerometerContract.Event.Next.HISTORY -> {
onEvent(AccelerometerContract.Event.OnHistoryViewModeChanged(value))
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,500 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import android.util.Log
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.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.core.entry.ChartEntryModelProducer
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 javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.FloatEntry
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
class AccelerometerEntry(
val frequency: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
}
@Composable
fun AccelerometerSpectre(
ble: BleInfo,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale
) {
val viewModel = hiltViewModel<AccelerometerSpectreViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial, accelMode, fftAxis, fftMode, frequency) {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
}
DisposableEffect(key1 = "ble", effect = {
onDispose {
Log.d("history", "dispose")
viewModel.setEvent(AccelerometerSpectreContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerSpectreContract.State.Display -> {
if (state.previousHistory !== null) {
"${fftMode.localized} (${state.previousHistory.size})"
}else {
fftMode.localized
}
}
AccelerometerSpectreContract.State.Exception -> fftMode.localized
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = title,
style = MaterialTheme.typography.titleLarge
)
if(state is AccelerometerSpectreContract.State.Display) {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
progress = progressAnimation,
)
}
else -> {}
}
}
}
IconButton(
onClick = {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
},
enabled = when(state){
is AccelerometerSpectreContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerSpectreContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerSpectreContract.State.Display -> Display(state = state)
AccelerometerSpectreContract.State.Exception -> Exception()
}
}
}
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun Display(
state: AccelerometerSpectreContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp
)
val scrollState = rememberChartScrollState()
Chart(
diffAnimationSpec = tween(0),
isZoomEnabled = true,
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize()
)
}
} else {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
}
else -> {}
}
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerSpectreContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
}
sealed class State : ViewState {
data class Display(
val previousHistory : List<Ble.Accelerometer.MeasurePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.MeasurePoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerSpectreViewModel @Inject constructor(
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
) : BaseViewModel<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
private var job: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
override fun handleEvents(event: AccelerometerSpectreContract.Event) {
when(event){
is AccelerometerSpectreContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerSpectreContract.State.Display) {
//if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when(state.loadingHistoryState){
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
} else {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerSpectreContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerSpectreContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnRefreshHistory
) {
/*viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}*/
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.StopMeasure
) {
job?.cancel()
}
}

View File

@ -0,0 +1,596 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
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.viewModelScope
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
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 javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.MeasureData
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
@Composable
fun AccelerometerAccel(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
) {
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value
viewModel.setEvent(AccelerometerAccelContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
DisposableEffect(key1 = "ble", effect = {
onDispose {
viewModel.setEvent(AccelerometerAccelContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f),
text = accelMode.localized,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
},
enabled = true
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerAccelContract.State.Display -> Display(state = state)
is AccelerometerAccelContract.State.Exception -> Exception()
}
}
}
}
@Composable
fun Display(
state: AccelerometerAccelContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
strokeCap = StrokeCap.Round
)
} else {
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is MeasureData.Accelerate -> {
FloatEntry(index.toFloat(), measurePoint.x )
}
is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value)
is MeasureData.Angle -> {
FloatEntry(index.toFloat(), measurePoint.xAngle )
}
}
})
yProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is MeasureData.Accelerate -> {
FloatEntry(index.toFloat(), measurePoint.y )
}
is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value)
is MeasureData.Angle -> {
FloatEntry(index.toFloat(), measurePoint.yAngle )
}
}
})
zProducer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
when(measurePoint){
is MeasureData.Accelerate -> {
FloatEntry(index.toFloat(), measurePoint.z )
}
is MeasureData.Vibration -> FloatEntry(index.toFloat(), measurePoint.value)
is MeasureData.Angle -> {
FloatEntry(index.toFloat(), measurePoint.zAngle )
}
}
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
)
)
val lastMeasure = state.measureHistory.lastOrNull()
if(lastMeasure is MeasureData.Accelerate) {
if(state.mode == AccelViewMode.ANGLE){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось X: ${lastMeasure.x}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
angle = lastMeasure.x,
modifier = Modifier.weight(1f),
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Y: ${lastMeasure.y}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.y
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Ось Z: ${lastMeasure.z}")
Spacer(modifier = Modifier.height(8.dp))
Angle(
modifier = Modifier.weight(1f),
angle = lastMeasure.z
)
}
}
} else {
Column() {
Text(text = "Ось X:")
Chart(
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Y:")
Chart(
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Z:")
Chart(
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
} else {
Column {
Text(text = "Вибрация:")
Chart(
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
}
@Composable
public fun Angle(
modifier: Modifier = Modifier,
angle: Float
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Column(
modifier = modifier.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "0°C")
Row(
modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "-90°C")
Surface(
modifier = Modifier
.aspectRatio(1f)
.fillMaxHeight()
.padding(8.dp),
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
) {
Box(
modifier = Modifier,
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.fillMaxWidth()
.rotate(-90f + angle),
horizontalArrangement = Arrangement.End
) {
Box(modifier = Modifier.weight(1f))
Divider(modifier = Modifier.weight(1f))
}
}
}
Text(text = "90°C")
}
Text(text = "±180°C")
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
}
sealed class State : ViewState {
data class Display(
val mode: AccelViewMode,
val measureHistory : List<MeasureData>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerAccelViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerAccelContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnStart
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, false)
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefreshHistory
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, true)
}
private fun startReadMeasure(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
restartJob: Boolean
){
if(restartJob || measureJob == null) {
measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch {
setState {
AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
}
getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency).onEach {
it.fold(
onSuccess = {
setState {
when (this) {
is AccelerometerAccelContract.State.Display -> {
var dataList = this.measureHistory.toMutableList().apply {
add(it)
}
if(accelMode != AccelViewMode.ANGLE) {
dataList = dataList.takeLast(10).toMutableList()
}
AccelerometerAccelContract.State.Display(accelMode, dataList)
}
AccelerometerAccelContract.State.Exception -> {
AccelerometerAccelContract.State.Display(accelMode, listOf(it))
}
}
}
},
onFailure = {
setState {
AccelerometerAccelContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}

View File

@ -0,0 +1,589 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
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.viewModelScope
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
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 javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.CloudUpload
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.decoration.ThresholdLine
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.formatter
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerHistoryBySerial
import llc.arma.ble.domain.usecase.MeasureData
import java.util.Date
class AccelEntry(
val localDate: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelEntry(localDate, x, y)
}
@Composable
fun AccelerometerHistory(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
}
/*DisposableEffect("ble") {
onDispose {
viewModel.setEvent(AccelerometerHistoryContract.Event.StopMeasure)
}
}*/
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "График измерений (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "График измерений"
is ProgressState.Progress -> "График измерений"
}
}
AccelerometerHistoryContract.State.Exception -> "График измерений"
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnExport)
},
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false
}
) {
Icon(
imageVector = Icons.Rounded.CloudUpload,
contentDescription = null
)
}
IconButton(
onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
},
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerHistoryContract.State.Display -> Display(state = state)
is AccelerometerHistoryContract.State.Exception -> Exception()
}
}
}
}
@Composable
fun Display(
state: AccelerometerHistoryContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelEntry)
?.localDate
?.let { formatter.format(Date(it)) }
.orEmpty()
}
val xProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val yProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
val zProducer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
xProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.HistoryPoint.Accelerate -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.x )
}
is Ble.Accelerometer.HistoryPoint.Vibration -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}
is Ble.Accelerometer.HistoryPoint.Angle -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.x )
}
}
})
yProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.HistoryPoint.Accelerate -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.y )
}
is Ble.Accelerometer.HistoryPoint.Vibration -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}
is Ble.Accelerometer.HistoryPoint.Angle -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.y )
}
}
})
zProducer.setEntries(state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
when(measurePoint){
is Ble.Accelerometer.HistoryPoint.Accelerate -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.z )
}
is Ble.Accelerometer.HistoryPoint.Vibration -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}
is Ble.Accelerometer.HistoryPoint.Angle -> {
AccelEntry(measurePoint.date, index.toFloat(), measurePoint.z )
}
}
})
val lineChart = lineChart(
decorations = listOf(
ThresholdLine(
thresholdValue = 0f
)
)
)
val lastMeasure = state.loadingHistoryState.data.lastOrNull()
if((lastMeasure is Ble.Accelerometer.HistoryPoint.Vibration).not()) {
Column() {
Text(text = "Ось X:")
Chart(
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Y:")
Chart(
chart = lineChart,
chartModelProducer = yProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
Text(text = "Ось Z:")
Chart(
chart = lineChart,
chartModelProducer = zProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
} else {
Column {
Text(text = "Вибрация:")
Chart(
chart = lineChart,
chartModelProducer = xProducer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = progressAnimation,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerHistoryContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
object OnExport : Event()
data class OnStart(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
}
sealed class State : ViewState {
data class Display(
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.HistoryPoint>>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
private val exportToXlsx: ExportToXlsx
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerHistoryContract.State.Display(
ProgressState.Indeterminate
)
override fun handleEvents(event: AccelerometerHistoryContract.Event) {
when(event){
is AccelerometerHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnExport -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnExport
) {
if(state is AccelerometerHistoryContract.State.Display){
if(state.loadingHistoryState is ProgressState.Finished){
exportToXlsx.invoke(state.loadingHistoryState.data)
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerHistoryContract.State.Exception
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -1,301 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.core.tween
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.viewModelScope
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
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 javax.inject.Inject
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.ui.text.style.TextAlign
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
@Composable
fun AccelerometerMeasure(
ble: BleInfo
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerMeasureContract.Event.OnStart(ble.serial))
}
DisposableEffect(key1 = ble.serial, effect = {
onDispose {
viewModel.setEvent(AccelerometerMeasureContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerMeasureContract.State.Display -> {
"История измерений"
}
AccelerometerMeasureContract.State.Exception -> "История измерений"
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerMeasureContract.Event.OnRefreshHistory(ble.serial))
},
enabled = true
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerMeasureContract.State.Display -> Display(state = state)
AccelerometerMeasureContract.State.Exception -> Exception()
}
}
}
}
@Composable
fun Display(
state: AccelerometerMeasureContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
if (state.measureHistory.isEmpty()) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
producer.setEntries(state.measureHistory.mapIndexed { index, measurePoint ->
FloatEntry(index.toFloat(), measurePoint)
})
val lineChart = columnChart()
Chart(
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(),
modifier = Modifier.fillMaxSize(),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased
)
)
}
}
}
@Composable
fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerMeasureContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String
) : Event()
data class OnRefreshHistory(
val serial: String
) : Event()
}
sealed class State : ViewState {
data class Display(
val measureHistory : List<Float>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerMeasureContract.State, AccelerometerMeasureContract.Event, AccelerometerMeasureContract.Effect>() {
var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerMeasureContract.State.Display(
emptyList()
)
override fun handleEvents(event: AccelerometerMeasureContract.Event) {
when(event){
is AccelerometerMeasureContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerMeasureContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerMeasureContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.StopMeasure
) {
measureJob?.cancel()
setState {
AccelerometerMeasureContract.State.Display(emptyList())
}
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.OnStart
) {
startReadMeasure(event.serial, false)
}
private fun reduce(
state: AccelerometerMeasureContract.State,
event: AccelerometerMeasureContract.Event.OnRefreshHistory
) {
startReadMeasure(event.serial, true)
}
private fun startReadMeasure(serial: String, restartJob: Boolean){
if(restartJob || measureJob == null) {
measureJob = viewModelScope.launch {
setState {
AccelerometerMeasureContract.State.Display(emptyList())
}
getAccelerometerMeasureBySerialFlow(serial).onEach {
it.fold(
onSuccess = {
setState {
when (this) {
is AccelerometerMeasureContract.State.Display -> {
val dataList = this.measureHistory.toMutableList().apply {
add(it)
}
AccelerometerMeasureContract.State.Display(dataList)
}
AccelerometerMeasureContract.State.Exception -> {
AccelerometerMeasureContract.State.Display(listOf(it))
}
}
}
},
onFailure = {
setState {
AccelerometerMeasureContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}

View File

@ -9,7 +9,6 @@ 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
@ -62,7 +61,146 @@ fun DisplayState(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnShowAccelerometerMeasure)
onEvent(AccelerometerContract.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 = "Сохранять историю измерений"
)
val history = ble.accelerometerState.saveHistory
if(history is Ble.Accelerometer.History.Enabled){
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "View mode ${history.mode.localized}"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "Scale: ${history.scale.localized}"
)
}
}
Switch(
checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.History.Enabled,
onCheckedChange = {
onEvent(AccelerometerContract.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(AccelerometerContract.Event.OnSaveIntervalEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерений"
)
val hours = ble.accelerometerState.historyInterval / 1000 / 60 / 60
val minutes = (ble.accelerometerState.historyInterval - ( hours * 1000 * 60 * 60 )) / 1000 / 60
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин."
)
}
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(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
.padding(8.dp)
) {
@ -86,6 +224,112 @@ fun DisplayState(
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { onEvent(AccelerometerContract.Event.OnAccelEdit) }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Ускорение"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnSpectreEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Спектр"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
}
)
@ -99,7 +343,7 @@ fun DisplayState(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
}
) {

View File

@ -0,0 +1,174 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
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.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun HistoryEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val history = state.accelerometer.accelerometerState.saveHistory
val detailed = if (history is Ble.Accelerometer.History.Enabled) history.detailed else false
val accelMode = if (history is Ble.Accelerometer.History.Enabled) history.mode else state.accelViewMode
val accelScale = if (history is Ble.Accelerometer.History.Enabled) history.scale else state.accelScale
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "История измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(
AccelerometerContract.Event.OnAccelViewModeEdit(
next = AccelerometerContract.Event.Next.HISTORY
)
)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel view mode"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelMode.localized
)
}
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(AccelerometerContract.Event.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.HISTORY))
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel scale"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelScale.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnHideHistoryEdit)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}

View File

@ -0,0 +1,225 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
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 llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
@Composable
fun IntervalEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelerometerState.historyInterval) {
mutableIntStateOf((state.accelerometerState.historyInterval).toInt())
}
val maxInterval = 10 * 24 * 60 * 60 * 1000
if(value > maxInterval){
value = maxInterval
}
if(value < 1){
value = 1 * 60 * 1000
}
val maxMinutes = maxInterval / millisInMinute
val maxHours = maxInterval / millisInHour
val maxDays = maxInterval / millisInDay
val dayValue = value / millisInDay
val hourValue = (value - (dayValue * millisInDay)) / millisInHour
val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute
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(
range = -1..maxDays,
value = dayValue,
onValueChanged = {
value = (it * millisInDay) + (hourValue * millisInHour) + (minutesValue * millisInMinute)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Дни")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxHours,
value = hourValue,
onValueChanged = {
value = (it * millisInHour) + (dayValue * millisInDay) + (minutesValue * millisInMinute)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Часы")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxMinutes,
value = minutesValue,
onValueChanged = {
value = (it * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Минуты")
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
AccelerometerContract.Event.OnSaveIntervalChanged(
value.toLong()
)
)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Применить"
)
}
}
}
}
const val millisInMinute = 1000 * 60
const val millisInHour = millisInMinute * 60
const val millisInDay = millisInHour * 24
@Composable
fun NumberPicker(
modifier: Modifier = Modifier,
range: IntRange,
value: Int,
onValueChanged: (Int) -> Unit
) {
LaunchedEffect(range){
if(value > range.last){
onValueChanged(range.last)
}
if(value < range.first){
onValueChanged(range.first)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
){
FilledIconButton(
onClick = {
if(value < range.last) onValueChanged(value + 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = null
)
}
Spacer(modifier = Modifier.height(36.dp))
AnimatedContent(
targetState = value,
transitionSpec = {
if (targetState > initialState) {
(slideInVertically { height -> height } + fadeIn()).togetherWith(
slideOutVertically { height -> -height } + fadeOut())
} else {
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
slideOutVertically { height -> height } + fadeOut())
}.using(
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(
style = MaterialTheme.typography.displaySmall,
text = "$targetCount"
)
}
Spacer(modifier = Modifier.height(36.dp))
FilledIconButton(
onClick = {
if(value > range.first) onValueChanged(value - 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}

View File

@ -0,0 +1,97 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.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.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
@Composable
fun PowerEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.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() + " dBb (${it.powerPercentage} %)")
}
}
Spacer(modifier = Modifier.height(16.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(
AccelerometerContract.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,431 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import llc.arma.ble.R
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
import llc.arma.ble.domain.model.Ble
@Composable
fun Write(
state: AccelerometerContract.State.Display.WriteState,
onEvent: (AccelerometerContract.Event) -> Unit
) {
Column(
modifier = Modifier.animateContentSize()
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Запись изменений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is AccelerometerContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.saveHistory != null || state.writeRequest.historyInterval != null) {
state.writeRequest.tx?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName} db"
)
}
}
}
}
state.writeRequest.saveHistory?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = when(it){
Ble.Accelerometer.History.Disabled -> "Выключено"
is Ble.Accelerometer.History.Enabled -> "Включено"
}
)
}
}
}
}
state.writeRequest.historyInterval?.let {
Box(
modifier = Modifier.padding(
vertical = 0.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
val hours = it / 1000 / 60 / 60
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
Text(
text = "Интервал измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин."
)
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
onClick = {
onEvent(AccelerometerContract.Event.OnWriteBle)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.background,
style = MaterialTheme.typography.labelLarge,
text = "Записать"
)
}
}
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
} else {
Spacer(modifier = Modifier.height(38.dp))
Text(
text = "Нет изменений",
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(64.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
is AccelerometerContract.State.Display.WriteState.Writing -> {
Box {
Column() {
Spacer(modifier = Modifier.height(28.dp))
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(48.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge,
text = "Отменить"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Success -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(llc.arma.ble.R.drawable.ic_done),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Успешно завершено"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
AccelerometerContract.State.Display.WriteState.Failure -> {
Box {
Column {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.size(125.dp)
.align(Alignment.Center),
painter = painterResource(R.drawable.ic_error),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
text = "Ошибка записи"
)
Spacer(modifier = Modifier.height(20.dp))
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.height(50.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primary,
onClick = {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Text(
modifier = Modifier.align(Alignment.Center),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.labelLarge,
text = "Ок"
)
}
}
}
}
}
}
}
}

View File

@ -55,7 +55,7 @@ fun PowerEdit(
onClick = { value = it }
)
Text(text = it.value.toString() + " db")
Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)")
}

View File

@ -52,7 +52,6 @@ val Ble.BleState.TX.localizedName: String
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
fun ThermometerScreen(
ble: Ble.Thermometer,

View File

@ -54,7 +54,7 @@ fun PowerEdit(
onClick = { value = it }
)
Text(text = it.value.toString() + " db")
Text(text = it.value.toString() + " dBb (${it.powerPercentage} %)")
}

View File

@ -204,7 +204,7 @@ fun Display(
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
), label = ""
)
CircularProgressIndicator(
@ -221,9 +221,7 @@ fun Display(
}
@Composable
fun Exception(
) {
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)

View File

@ -106,7 +106,7 @@ fun Write(
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it.localizedName}"
text = it.localizedName
)
}
@ -137,13 +137,17 @@ fun Write(
modifier = Modifier.weight(1f)
) {
val hours = it / 1000 / 60 / 60
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
Text(
text = "Интервал измерний"
text = "Интервал измерений"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${it / 1000 / 60 / 60} ч."
text = "$hours ч. $minutes мин."
)
}

View File

@ -11,7 +11,6 @@ import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.util.toRange
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@ -24,17 +23,80 @@ import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import llc.arma.ble.domain.repository.BleRepository
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.MeasureData
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.random.Random
val FftFrequency.sendData: Byte
get() {
return when(this){
FftFrequency.OFF -> 0
FftFrequency.F_1 -> 1
FftFrequency.F_10 -> 2
FftFrequency.F_25 -> 3
FftFrequency.F_50 -> 4
FftFrequency.F_100 -> 5
FftFrequency.F_200 -> 6
FftFrequency.F_400 -> 7
FftFrequency.F_1620 -> 8
FftFrequency.F_1344 -> 9
}
}
val FftAxis.sendData: Byte
get() {
return when(this){
FftAxis.AUTO -> 0
FftAxis.X -> 1
FftAxis.Y -> 2
FftAxis.Z -> 3
}
}
val FftViewMode.sendData: Byte
get() {
return when(this){
FftViewMode.SPECTRE -> 0
FftViewMode.X -> 1
FftViewMode.Y -> 2
FftViewMode.Z -> 3
}
}
val AccelViewMode.sendData: Byte
get() {
return when(this){
AccelViewMode.ACCELERATION -> 0
AccelViewMode.PEAK_ACCELERATION -> 1
AccelViewMode.RMS -> 2
AccelViewMode.VIBRATION -> 3
AccelViewMode.ANGLE -> 0
}
}
val AccelScale.sendData: Byte
get() {
return when(this){
AccelScale.S_2 -> 0
AccelScale.S_4 -> 1
AccelScale.S_8 -> 2
AccelScale.S_16 -> 3
}
}
val serviceUUID: UUID = UUID.fromString("a77db03a-9bc4-11ed-a8fc-0242ac120002")
val accelerometerDescriptorUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
val accelerometerReadUUID: UUID = UUID.fromString("00002713-0000-1000-8000-00805f9b34fb")
val temperatureHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val accelerometerHistoryReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val temperatureReadUUID: UUID = UUID.fromString("00002a6e-0000-1000-8000-00805f9b34fb")
val intervalReadUUID: UUID = UUID.fromString("a77db2d8-9bc4-11ed-a8fc-0242ac120002")
val intervalWriteUUID: UUID = UUID.fromString("a77db6f2-9bc4-11ed-a8fc-0242ac120002")
@ -56,11 +118,20 @@ fun UByteArray.toTemperature(): Float {
return result
}
fun ByteArray.get2byteUIntAt(idx: Int) =
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
@Singleton
class BleRepositoryImpl @Inject constructor(
private val app: Application
) : BleRepository {
private val ScanResult.timerEnabled: Boolean
get() {
return scanRecord?.manufacturerSpecificData?.get(89)?.get(2) == 1.toByte()
}
private val ScanResult.info: BleInfo
get() {
return BleInfo(
@ -69,15 +140,12 @@ class BleRepositoryImpl @Inject constructor(
batteryLevel = batteryLevel ?: 0,
rssi = rssi,
type = type,
scanTime = timestampNanos / 1_000_000
scanTime = timestampNanos / 1_000_000,
tx = scanRecord?.txPowerLevel ?: 0,
recordEnabled = timerEnabled
)
}
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)
@ -234,6 +302,24 @@ class BleRepositoryImpl @Inject constructor(
return when(result.info.type) {
BleInfo.Type.ACCELEROMETER -> {
val tState = suspendCancellableCoroutine {
CoroutineScope(Dispatchers.IO).launch {
it.resume(readAccelState(result))
}
}.fold(
onFailure = {
return Result.failure(it)
},
onSuccess = {
it
}
)
Result.success(
flow {
@ -250,8 +336,21 @@ class BleRepositoryImpl @Inject constructor(
} else {
newResult.rssi
}
)
),
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
}
),
accelerometerState = tState
)
)
@ -379,10 +478,6 @@ class BleRepositoryImpl @Inject constructor(
)
}
BleInfo.Type.ACCELEROMETER -> {
TODO()
}
}
}
@ -419,6 +514,78 @@ class BleRepositoryImpl @Inject constructor(
}
private suspend fun readAccelState(
record: ScanResult
): Result<Ble.Accelerometer.AccelerometerState, BleException> {
val history = readHistoryInterval(record).fold(
onFailure = {
return Result.failure(it)
},
onSuccess = { return@fold it }
)
val historyParams = when(record.timerEnabled){
true -> {
writeCharacteristic(
device = record.device,
serviceId = serviceUUID,
characteristicId = accelerometerReadUUID,
writeData = byteArrayOf(4)
).onFailure {
return Result.failure(BleException.UnexpectedResponse)
}
readCharacteristic(
device = record.device,
serviceId = serviceUUID,
characteristicId = accelerometerReadUUID,
).fold(
onSuccess = {
Log.d("history", it.joinToString { it.toString() })
val scale = when(it[1].toInt()){
0 -> AccelScale.S_2
1 -> AccelScale.S_4
2 -> AccelScale.S_8
3 -> AccelScale.S_16
else -> {
return Result.failure(BleException.UnexpectedResponse)
}
}
val mode = when(it[0].toInt()){
0 -> AccelViewMode.ACCELERATION
1 -> AccelViewMode.PEAK_ACCELERATION
2 -> AccelViewMode.RMS
3 -> AccelViewMode.VIBRATION
4 -> AccelViewMode.ANGLE
else -> {
return Result.failure(BleException.UnexpectedResponse)
}
}
Ble.Accelerometer.History.Enabled(
scale = scale,
mode = mode,
detailed = false //TODO
)
},
onFailure = {
return Result.failure(BleException.UnexpectedResponse)
}
)
}
false -> Ble.Accelerometer.History.Disabled
}
return Result.success(
Ble.Accelerometer.AccelerometerState(
saveHistory = historyParams,
historyInterval = history
)
)
}
@OptIn(ExperimentalUnsignedTypes::class)
private suspend fun readTemperature(
record: ScanResult
@ -484,9 +651,14 @@ class BleRepositoryImpl @Inject constructor(
}
override suspend fun getTemperatureHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> {
override suspend fun getAccelerometerSpectreBySerial(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>> {
var gatt: BluetoothGatt? = null
@ -496,7 +668,9 @@ class BleRepositoryImpl @Inject constructor(
if (checkPermission()) {
gatt = it.connectGatt(app, false, ReadHistoryCallback(app) {
gatt = it.connectGatt(app, false, ReadAccelerometerSpectreCallback(
app, accelScale, accelMode, fftAxis, fftMode, frequency
) {
CoroutineScope(Dispatchers.IO).launch {
send(it)
}
@ -514,6 +688,143 @@ class BleRepositoryImpl @Inject constructor(
}
awaitClose {
Log.d("measure", "close")
gatt?.close()
}
}
}
override suspend fun getTemperatureHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>> {
var gatt: BluetoothGatt? = null
return callbackFlow {
deviceCache[serial]?.device?.let {
if (checkPermission()) {
gatt = it.connectGatt(app, false, ReadTemperatureHistoryCallback(app) {
CoroutineScope(Dispatchers.IO).launch {
send(it)
}
})
} else {
CoroutineScope(Dispatchers.IO).launch {
send(Result.failure(BleException.PermissionDenied))
}
return@callbackFlow
}
}
awaitClose {
gatt?.close()
}
}
}
override suspend fun getAccelerometerHistoryBySerial(
serial: String
): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
var gatt: BluetoothGatt? = null
return callbackFlow {
deviceCache[serial]?.let { result ->
val device = result.device
if (checkPermission()) {
val state = readAccelState(result).fold(
onSuccess = {
},
onFailure = {
null
}
)
var scale: AccelScale? = null
var mode: AccelViewMode? = null
writeCharacteristic(
device = device,
serviceId = serviceUUID,
characteristicId = accelerometerReadUUID,
writeData = byteArrayOf(4)
).fold(
onSuccess = {
readCharacteristic(
device = device,
serviceId = serviceUUID,
characteristicId = accelerometerReadUUID,
).fold(
onSuccess = {
scale = when(it[1].toInt()){
0 -> AccelScale.S_2
1 -> AccelScale.S_4
2 -> AccelScale.S_8
3 -> AccelScale.S_16
else -> null
}
mode = when(it[0].toInt()){
0 -> AccelViewMode.ACCELERATION
1 -> AccelViewMode.PEAK_ACCELERATION
2 -> AccelViewMode.RMS
3 -> AccelViewMode.VIBRATION
4 -> AccelViewMode.ANGLE
else -> null
}
},
onFailure = { null }
)
},
onFailure = {
null
}
)
if(scale != null && mode != null) {
gatt = device.connectGatt(
app,
false,
ReadAccelerometerHistoryCallback(mode!!, scale!!, app) {
CoroutineScope(Dispatchers.IO).launch {
send(it)
}
})
} else {
CoroutineScope(Dispatchers.IO).launch {
send(Result.failure(BleException.UnexpectedResponse))
}
}
} else {
CoroutineScope(Dispatchers.IO).launch {
send(Result.failure(BleException.PermissionDenied))
}
}
}
awaitClose {
gatt?.close()
}
@ -594,6 +905,42 @@ class BleRepositoryImpl @Inject constructor(
}
override suspend fun writeBle(
serial: String,
request: Ble.Accelerometer.WriteRequest
): Result<Unit, BleException> = suspendCancellableCoroutine {
deviceCache[serial]?.let { scanResult ->
if(checkPermission()) {
var gatt: BluetoothGatt? = null
val callback = WriteAccelerometerCallback(app, request) { result ->
gatt?.close()
result.onSuccess {
deviceCache.remove(serial)
resultList.remove(serial)
}
it.resume(result)
}
gatt = scanResult.device.connectGatt(app, false, callback)
} else {
it.resume(Result.failure(BleException.PermissionDenied))
}
}
}
override suspend fun changeBlePassword(
password: String,
serial: String
@ -618,52 +965,51 @@ class BleRepositoryImpl @Inject constructor(
return Result.success(Unit)
}
override fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Float, BleException>> {
override fun getAccelerometerMeasureBySerialFlow(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
): Flow<Result<MeasureData, BleException>> {
return callbackFlow {
deviceCache[serial]?.let {
var gatt: BluetoothGatt? = null
it.device.connectGatt(app, false, object : BluetoothGattCallback() {
if(checkPermission()) {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
gatt.discoverServices()
}
deviceCache[serial]?.let {
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
super.onServicesDiscovered(gatt, status)
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
gatt.setCharacteristicNotification(it, true)
gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
gatt = it.device.connectGatt(
app,
false,
ReadAccelerometerCallback(
app = app,
accelScale = accelScale,
accelMode = accelMode,
fftAxis = fftAxis,
fftMode = fftMode,
frequency = frequency
) { result ->
CoroutineScope(Dispatchers.IO).launch {
send(result)
}
}
}
)
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
}
Log.d("new", value.toString())
awaitClose {
gatt?.close()
}
CoroutineScope(Dispatchers.IO).launch {
} else {
CoroutineScope(Dispatchers.IO).launch {
send(
Result.success(((-256)..256).random().toFloat())
)
}
}
})
}
awaitClose {
send(Result.failure(BleException.PermissionDenied))
}
}
@ -688,8 +1034,6 @@ class BleRepositoryImpl @Inject constructor(
newState: Int
) {
Log.d("read", "onConnectionStateChange $newState $status")
if(status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
@ -699,16 +1043,18 @@ class BleRepositoryImpl @Inject constructor(
gatt.discoverServices()
} else {
it.resume(Result.failure(BleException.PermissionDenied))
gatt.disconnect()
}
} else {
gatt.close()
}
} else {
it.resume(Result.failure(BleException.PermissionDenied))
gatt.disconnect()
}
@ -720,8 +1066,6 @@ class BleRepositoryImpl @Inject constructor(
) {
super.onServicesDiscovered(gatt, status)
Log.d("read", "onServicesDiscovered $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.services?.firstOrNull { service ->
@ -736,6 +1080,7 @@ class BleRepositoryImpl @Inject constructor(
} else {
gatt.disconnect()
it.resume(Result.failure(BleException.PermissionDenied))
}
@ -744,10 +1089,14 @@ class BleRepositoryImpl @Inject constructor(
}
gatt.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse))
}else{
} else {
gatt.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse))
}
}
@ -787,8 +1136,6 @@ class BleRepositoryImpl @Inject constructor(
if(status == BluetoothGatt.GATT_SUCCESS) {
if (checkPermission()) {
gatt.close()
result = value
it.resume(Result.success(result!!))
@ -796,11 +1143,11 @@ class BleRepositoryImpl @Inject constructor(
it.resume(Result.failure(BleException.PermissionDenied))
}
gatt.disconnect()
} else {
it.resume(Result.failure(BleException.UnexpectedResponse))
gatt.disconnect()
}
}
@ -832,8 +1179,6 @@ class BleRepositoryImpl @Inject constructor(
newState: Int
) {
Log.d("write", "onConnectionStateChange $status $newState")
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
@ -844,7 +1189,7 @@ class BleRepositoryImpl @Inject constructor(
} else {
bleGatt?.close()
bleGatt?.disconnect()
it.resume(Result.failure(BleException.PermissionDenied))
}
@ -852,13 +1197,12 @@ class BleRepositoryImpl @Inject constructor(
} else {
bleGatt?.close()
it.resume(Result.success(Unit))
}
} else {
bleGatt?.close()
bleGatt?.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse))
}
@ -871,8 +1215,6 @@ class BleRepositoryImpl @Inject constructor(
) {
super.onServicesDiscovered(gatt, status)
Log.d("write", "onServicesDiscovered $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
gatt.services?.firstOrNull { service ->
@ -887,7 +1229,7 @@ class BleRepositoryImpl @Inject constructor(
} else {
bleGatt?.close()
bleGatt?.disconnect()
it.resume(Result.failure(BleException.PermissionDenied))
}
@ -898,12 +1240,12 @@ class BleRepositoryImpl @Inject constructor(
Log.d("write", "service not found")
bleGatt?.close()
bleGatt?.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse))
} else {
bleGatt?.close()
bleGatt?.disconnect()
it.resume(Result.failure(BleException.UnexpectedResponse))
}
@ -917,17 +1259,13 @@ class BleRepositoryImpl @Inject constructor(
) {
super.onCharacteristicWrite(gatt, characteristic, status)
Log.d("write", "onCharacteristicWrite $status")
if (checkPermission()) {
gatt.close()
if(status == BluetoothGatt.GATT_SUCCESS) {
it.resume(Result.success(Unit))
}else{
} else {
it.resume(Result.failure(BleException.UnexpectedResponse))
@ -935,11 +1273,12 @@ class BleRepositoryImpl @Inject constructor(
} else {
gatt.close()
it.resume(Result.failure(BleException.PermissionDenied))
}
gatt.disconnect()
}
}

View File

@ -0,0 +1,41 @@
package llc.arma.ble.data
import android.app.Application
import android.content.Intent
import androidx.core.content.FileProvider
import llc.arma.ble.R
import llc.arma.ble.domain.repository.EmailRepository
import llc.arma.ble.domain.repository.XlsxRepository
import llc.arma.ble.domain.usecase.MeasureData
import org.apache.poi.ss.SpreadsheetVersion
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.apache.poi.ss.util.AreaReference
import org.apache.poi.ss.util.CellReference
import org.apache.poi.util.IOUtils
import org.apache.poi.xssf.usermodel.XSSFSheet
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.UUID
import javax.inject.Inject
class EmailRepositoryImpl @Inject constructor(
private val application: Application
) : EmailRepository {
override fun sendFile(file: File) {
val uri = FileProvider.getUriForFile(application, "llc.arma.ble.fileprovider", file)
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
sendIntent.type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
sendIntent.putExtra(Intent.EXTRA_SUBJECT, "Measure history")
sendIntent.putExtra(Intent.EXTRA_STREAM, uri)
application.startActivity(
Intent.createChooser(sendIntent, "Send email...").apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
)
}
}

View File

@ -0,0 +1,297 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.MeasureData
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import java.util.UUID
import kotlin.math.PI
import kotlin.math.atan
import kotlin.math.pow
import kotlin.math.sqrt
class ReadAccelerometerCallback(
private val app: Application,
private val accelScale: AccelScale,
private val accelMode: AccelViewMode,
private val fftAxis: FftAxis,
private val fftMode: FftViewMode,
private val frequency: FftFrequency,
private val onResult: (Result<MeasureData, BleException>) -> Unit
) : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (checkPermission()) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
Log.d("accel", "onServicesDiscovered")
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {
val payload = byteArrayOf(
4,
accelMode.sendData,
accelScale.sendData,
fftMode.sendData,
fftAxis.sendData,
frequency.sendData,
1
)
gatt.writeCharacteristic(it, payload)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
Log.d("accel", "request written")
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {
gatt.setCharacteristicNotification(it, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")), BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
} else {
val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
@Deprecated("Deprecated in Java")
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicChanged(gatt, characteristic)
onCommonCharacteristicRead(characteristic.value)
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
onCommonCharacteristicRead(value)
}
private fun onCommonCharacteristicRead(
value: ByteArray,
){
val result = if(accelMode == AccelViewMode.VIBRATION){
MeasureData.Vibration((value.get2byteShortAt().toFloat() * accelScale.k) / Short.MAX_VALUE)
} else {
val data = value.toList().chunked(2).map {
it.toByteArray().get2byteShortAt()
}
Log.d("accel", "x: ${data[0]} y: ${data[1]} z: ${data[2]} bytes: ${value.joinToString { it.toString() }}")
val x: Float
val y: Float
val z: Float
if (accelMode == AccelViewMode.ANGLE) {
x = calculateZAngle(data[2].toFloat(), data[1].toFloat()) * 180f / Math.PI.toFloat()
y = calculateZAngle(data[2].toFloat(), data[0].toFloat()) * 180f / Math.PI.toFloat()
z = calculateZAngle(data[0].toFloat(), data[1].toFloat()) * 180f / Math.PI.toFloat()
} else {
x = (data[0].toFloat() * accelScale.k) / Short.MAX_VALUE
y = (data[1].toFloat() * accelScale.k) / Short.MAX_VALUE
z = (data[2].toFloat() * accelScale.k) / Short.MAX_VALUE
}
val state = MeasureData.Accelerate(
x = x,
y = y,
z = z
)
state
}
onResult(Result.success(result))
}
private fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
private fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException>{
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
}
fun calculateAngle(
targetAxis: Float,
firstAxis: Float,
secondAxis: Float
): Float {
return atan(targetAxis.div(sqrt(firstAxis.pow(2) + secondAxis.pow(2))))
}
public fun calculateZAngle(
x: Float,
y: Float
): Float {
var x = x
if(x == 0f && y == 0f){
x = 0.0001f
}
if(x > 0){
return atan(y/x)
}
if(x < 0 && y >= 0){
return atan(y/x) + PI.toFloat()
}
if(x < 0 && y < 0){
return atan(y/x) - PI.toFloat()
}
if(x == 0f && y > 0){
return PI.toFloat() / 2f
}
if(x == 0f && y < 0){
return -PI.toFloat() / 2f
}
return 0f
}

View File

@ -0,0 +1,410 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
class ReadAccelerometerHistoryCallback(
private val mode: AccelViewMode,
private val scale: AccelScale,
private val app: Application,
private val onResult: (Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
enum class Property {
DATA_SIZE, PACKAGE
}
private fun ByteArray.get4byteUIntAt(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 fun ByteArray.get2byteUIntAt(idx: Int) =
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
private var readProperty: Property? = null
init {
Log.d("history", scale.name)
onResult(Result.success(ProgressState.Indeterminate))
}
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (checkPermission()) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if(status == BluetoothGatt.GATT_SUCCESS){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerHistoryReadUUID)?.let {
if (checkPermission()) {
readProperty = Property.DATA_SIZE
gatt.writeCharacteristic(it, byteArrayOf(2))
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
}
}
}
private var lastMeasureSystemTime: Long? = null
private var bleMeasureInterval: Long? = null
private var bleRealTime: Long? = null
private var bleLastMeasureTime: Long? = null
private val resultTemperaturePackage: MutableList<Float> = mutableListOf()
private val result = mutableListOf<List<UByte>>()
var expectedDataSize: Int? = null
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicRead(gatt, characteristic, status)
onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status)
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
onCommonCharacteristicRead(gatt, characteristic, value, status)
}
@OptIn(ExperimentalUnsignedTypes::class)
private fun onCommonCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
){
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){
Property.DATA_SIZE -> {
if(value.contentEquals(byteArrayOf(0, 0))) {
onResult(
Result.success(
ProgressState.Finished(
emptyList()
)
)
)
gatt.close()
} else {
Log.d("expected data size", value.get2byteUIntAt(0).toString())
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
readProperty = Property.PACKAGE
gatt.writeCharacteristic(characteristic, writeData)
}
}
Property.PACKAGE -> {
if(value[0] == 250.toByte()){
result.add(value.toUByteArray().toList())
bleMeasureInterval = value.get4byteUIntAt(4).toLong()
bleLastMeasureTime = value.get4byteUIntAt(8).toLong()
bleRealTime = value.get4byteUIntAt(12).toLong()
lastMeasureSystemTime = System.currentTimeMillis() - ((bleRealTime!! - bleLastMeasureTime!!) * 1_000)
val nextPackageDataCount = value.get2byteUIntAt(2)
val temperatureDataArray = value.toUByteArray().asList().subList(16, value.size)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
it.toUByteArray().toByteArray().get2byteShortAt().toFloat()
}.toMutableList()
)
Log.d("received data size", (temperatureDataArray.chunked(2).size).toString())
Log.d("next data size", nextPackageDataCount.toString())
expectedDataSize = nextPackageDataCount.toInt() + resultTemperaturePackage.size
onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat())))
onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize!!.toFloat())))
if(nextPackageDataCount != 0.toUInt()){
if (checkPermission()) {
gatt.writeCharacteristic(characteristic, byteArrayOf(5))
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(
Result.success(
ProgressState.Finished(
when(mode){
AccelViewMode.ACCELERATION,
AccelViewMode.PEAK_ACCELERATION,
AccelViewMode.RMS -> {
resultTemperaturePackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
x = (it.value[0] * scale.k) / Short.MAX_VALUE,
y = (it.value[1] * scale.k) / Short.MAX_VALUE,
z = (it.value[2] * scale.k) / Short.MAX_VALUE
)
}
}
AccelViewMode.ANGLE -> {
resultTemperaturePackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
x = calculateZAngle(it.value[2], it.value[1]) * 180f / Math.PI.toFloat(),
y = calculateZAngle(it.value[2], it.value[0]) * 180f / Math.PI.toFloat(),
z = calculateZAngle(it.value[0], it.value[1]) * 180f / Math.PI.toFloat()
)
}
}
AccelViewMode.VIBRATION -> {
resultTemperaturePackage.withIndex().map {
Ble.Accelerometer.HistoryPoint.Vibration(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
value = (it.value * scale.k) / Short.MAX_VALUE
)
}
}
}
)
)
)
gatt.close()
}
} else {
if (value[0] == 251.toByte()) {
result.add(value.toUByteArray().toList())
val nextPackageDataCount = value.get2byteUIntAt(2)
val temperatureDataArray = value.toUByteArray().toList().subList(4, value.size)
resultTemperaturePackage.addAll(
temperatureDataArray.chunked(2).map {
it.toUByteArray().toByteArray().get2byteShortAt().toFloat()
}
)
Log.d("received data size", (temperatureDataArray.chunked(2).size).toString())
Log.d("next data size", nextPackageDataCount.toString())
onResult(Result.success(ProgressState.Progress(resultTemperaturePackage.size.toFloat() / expectedDataSize!!.toFloat())))
if (nextPackageDataCount != 0.toUInt()) {
val writeData = byteArrayOf(5)
gatt.writeCharacteristic(characteristic, writeData)
gatt.readCharacteristic(characteristic)
} else {
onResult(
Result.success(
ProgressState.Finished(
when(mode){
AccelViewMode.ACCELERATION,
AccelViewMode.PEAK_ACCELERATION,
AccelViewMode.RMS -> {
resultTemperaturePackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
x = (it.value[0] * scale.k) / Short.MAX_VALUE,
y = (it.value[1] * scale.k) / Short.MAX_VALUE,
z = (it.value[2] * scale.k) / Short.MAX_VALUE
)
}
}
AccelViewMode.ANGLE -> {
resultTemperaturePackage.chunked(3).withIndex().map {
Ble.Accelerometer.HistoryPoint.Angle(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
x = calculateZAngle(it.value[2], it.value[1]) * 180f / Math.PI.toFloat(),
y = calculateZAngle(it.value[2], it.value[0]) * 180f / Math.PI.toFloat(),
z = calculateZAngle(it.value[0], it.value[1]) * 180f / Math.PI.toFloat()
)
}
}
AccelViewMode.VIBRATION -> {
resultTemperaturePackage.withIndex().map {
Ble.Accelerometer.HistoryPoint.Vibration(
date = lastMeasureSystemTime!! - (((resultTemperaturePackage.size - 1) - it.index) * bleMeasureInterval!!),
value = (it.value * scale.k) / Short.MAX_VALUE
)
}
}
}
)
)
)
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
else -> {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if(status == BluetoothGatt.GATT_SUCCESS){
if (checkPermission()) {
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException>{
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
}

View File

@ -0,0 +1,464 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.util.UUID
fun ByteArray.get2byteShortAt(): Int {
val shorts = ShortArray(1)
ByteBuffer.wrap(this).order(LITTLE_ENDIAN).asShortBuffer()[shorts]
return shorts[0].toInt()
}
class ReadAccelerometerSpectreCallback(
private val app: Application,
private val accelScale: AccelScale,
private val accelMode: AccelViewMode,
private val fftAxis: FftAxis,
private val fftMode: FftViewMode,
private val frequency: FftFrequency,
private val onResult: (Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
enum class Property {
DATA_SIZE, PACKAGE
}
private fun ByteArray.get4byteUIntAt(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 fun ByteArray.get2byteUIntAt(idx: Int) =
((this[idx + 1].toUInt() and 0xFFu) shl 8) or
(this[idx].toUInt() and 0xFFu)
private var readProperty: Property? = null
init {
onResult(Result.success(ProgressState.Indeterminate))
}
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(status == BluetoothGatt.GATT_SUCCESS){
if(newState == BluetoothGatt.STATE_CONNECTED){
if (checkPermission()) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
if(status == BluetoothGatt.GATT_SUCCESS){
enableNotifications(gatt)
}
}
private fun enableNotifications(
gatt: BluetoothGatt
){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {
gatt.setCharacteristicNotification(it, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeDescriptor(it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")),
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
} else {
val descriptor = it.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
Log.d("spectre", "enable notification")
onResult(Result.success(ProgressState.Indeterminate))
resultAccelerometerPackage.clear()
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
return
}
onResult(Result.failure(BleException.UnexpectedResponse))
}
private var initialValue: Long? = null
private var frequencyInterval: Long? = null
private val resultAccelerometerPackage: MutableList<Float> = mutableListOf()
private var expectedDataSize: Int? = null
@Deprecated("Deprecated in Java")
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicRead(gatt, characteristic, status)
onCommonCharacteristicRead(gatt, characteristic, characteristic.value, status)
}
}
@Deprecated("Deprecated in Java")
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
super.onCharacteristicChanged(gatt, characteristic)
onCommonCharacteristicChanged(gatt, characteristic, characteristic.value)
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
onCommonCharacteristicChanged(gatt, characteristic, value)
}
private fun onCommonCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
){
if(characteristic.uuid == accelerometerReadUUID) {
readProperty = Property.DATA_SIZE
gatt.getService(serviceUUID).getCharacteristic(accelerometerHistoryReadUUID)?.let {
gatt.writeCharacteristic(it, byteArrayOf(2))
}
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, value, status)
onCommonCharacteristicRead(gatt, characteristic, value, status)
}
private fun onCommonCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
){
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){
Property.DATA_SIZE -> {
if(value.contentEquals(byteArrayOf(0, 0))) {
onResult(
Result.success(
ProgressState.Finished(
emptyList()
)
)
)
gatt.close()
} else {
val writeData = mutableListOf(
1.toByte(),
0.toByte(),
0.toByte()
).apply {
addAll(value.toList())
}.toByteArray()
readProperty = Property.PACKAGE
gatt.writeCharacteristic(characteristic, writeData)
}
}
Property.PACKAGE -> {
if(value[0] == 250.toByte()){
initialValue = value.get4byteUIntAt(8).toLong()
frequencyInterval = value.get4byteUIntAt(4).toLong()
val accelerometerDataArray = value.asList().subList(16, value.size)
resultAccelerometerPackage.addAll(
accelerometerDataArray.chunked(2).map {
it.toByteArray().get2byteShortAt().toFloat()
}.toMutableList()
)
val nextPackageDataCount = value.get2byteUIntAt(2)
expectedDataSize = nextPackageDataCount.toInt() + resultAccelerometerPackage.size
onResult(Result.success(ProgressState.Progress(0f / expectedDataSize!!.toFloat())))
onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat())))
if(nextPackageDataCount != 0.toUInt()){
if (checkPermission()) {
gatt.writeCharacteristic(characteristic, byteArrayOf(5))
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(
Result.success(
ProgressState.Finished(
resultAccelerometerPackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = frequencyInterval!! * it.index + initialValue!!,
value = (it.value * accelScale.k) / Short.MAX_VALUE
)
}
)
)
)
start(gatt)
}
} else {
if (value[0] == 251.toByte()) {
val nextPackageDataCount = value.get2byteUIntAt(2)
val temperatureDataArray = value.toList().subList(4, value.size)
resultAccelerometerPackage.addAll(
temperatureDataArray.chunked(2).map {
it.toByteArray().get2byteShortAt().toFloat()
}
)
onResult(Result.success(ProgressState.Progress(resultAccelerometerPackage.size.toFloat() / expectedDataSize!!.toFloat())))
if (nextPackageDataCount != 0.toUInt()) {
val writeData = byteArrayOf(5)
gatt.writeCharacteristic(characteristic, writeData)
gatt.readCharacteristic(characteristic)
} else {
onResult(
Result.success(
ProgressState.Finished(
resultAccelerometerPackage.withIndex().map {
Ble.Accelerometer.MeasurePoint(
frequency = frequencyInterval!! * it.index + initialValue!!,
value = (it.value * accelScale.k) / Short.MAX_VALUE
)
}
)
)
)
start(gatt)
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
else -> {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if(readProperty !== null) {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (checkPermission()) {
gatt.readCharacteristic(characteristic)
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
gatt.close()
}
}
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
super.onDescriptorWrite(gatt, descriptor, status)
start(gatt)
}
private fun start(
gatt: BluetoothGatt,
){
gatt.getService(serviceUUID)?.getCharacteristic(accelerometerReadUUID)?.let {
if (checkPermission()) {
val payload = byteArrayOf(
4,
accelMode.sendData,
accelScale.sendData,
fftMode.sendData,
fftAxis.sendData,
frequency.sendData,
2
)
readProperty = null
gatt.writeCharacteristic(it, payload)
onResult(Result.success(ProgressState.Indeterminate))
resultAccelerometerPackage.clear()
} else {
onResult(Result.failure(BleException.PermissionDenied))
gatt.close()
}
return
}
}
private fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
private fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException>{
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
}

View File

@ -13,17 +13,16 @@ import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import java.util.stream.Collectors
enum class Property {
DATA_SIZE, PACKAGE
}
class ReadHistoryCallback(
class ReadTemperatureHistoryCallback(
private val app: Application,
private val onResult: (Result<ProgressState<List<Ble.Thermometer.MeasurePoint>>, BleException>) -> Unit
) : BluetoothGattCallback() {
enum class Property {
DATA_SIZE, PACKAGE
}
private fun ByteArray.get4byteUIntAt(idx: Int) =
((this[idx + 3].toUInt() and 0xFFu) shl 24) or
((this[idx + 2].toUInt() and 0xFFu) shl 16) or
@ -133,7 +132,6 @@ class ReadHistoryCallback(
value: ByteArray,
status: Int
){
Log.d("read", value[0].toString())
if(status == BluetoothGatt.GATT_SUCCESS){
when(readProperty){

View File

@ -0,0 +1,245 @@
package llc.arma.ble.data
import android.Manifest
import android.app.Application
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothProfile
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import java.util.UUID
class WriteAccelerometerCallback(
private val app: Application,
private var request: Ble.Accelerometer.WriteRequest,
private val onResult: (Result<Unit, BleException>) -> Unit
) : BluetoothGattCallback() {
private var flashed = false
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
super.onConnectionStateChange(gatt, status, newState)
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt,
status: Int
) {
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)
}
private fun onCycle(
gatt: BluetoothGatt,
status: Int
){
Log.d("write", "${request.tx != null} ${request.saveHistory != null} ${request.historyInterval != null}")
if(request.tx != null || request.saveHistory != null || request.historyInterval != null) {
fun UInt.to4ByteArrayInLittleEndian(): ByteArray =
(3 downTo 0).map {
(this shr (it * Byte.SIZE_BITS)).toByte()
}.toByteArray()
var uuid: Triple<UUID, ByteArray, Ble.Accelerometer.WriteRequest>? = null
uuid = request.historyInterval?.let {
Triple(
intervalWriteUUID,
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray(),
request.copy(
historyInterval = null
)
)
}
uuid = request.saveHistory?.let {
Triple(
saveEnabledWriteUUID,
mutableListOf<Byte>(4).apply {
add(if (it is Ble.Accelerometer.History.Enabled) 1 else 0)
if(it is Ble.Accelerometer.History.Enabled) {
add(it.mode.sendData)
add(it.scale.sendData)
}
}.toByteArray(),
request.copy(
saveHistory = null
)
)
} ?: uuid
uuid = request.tx?.let {
Triple(
txWriteUUID,
byteArrayOf(
when (it) {
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
}
),
request.copy(
tx = null
)
)
} ?: uuid
uuid?.let { uuid ->
gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull {
it.uuid == uuid.first
}?.let {
gatt.writeCharacteristic(it, uuid.second)
request = uuid.third
return
}
}
onResult(Result.failure(BleException.UnexpectedResponse))
} else {
if(flashed.not()){
flashed = true
gatt.services.firstOrNull { it.uuid == serviceUUID }?.characteristics?.firstOrNull {
it.uuid == flashWriteUUID
}?.let {
gatt.writeCharacteristic(it, byteArrayOf(9))
return
}
onResult(Result.failure(BleException.UnexpectedResponse))
} else {
onResult(Result.success(Unit))
}
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS || flashed) {
onCycle(gatt, status)
} else {
onResult(Result.failure(BleException.UnexpectedResponse))
}
} else {
onResult(Result.failure(BleException.PermissionDenied))
}
}
fun BluetoothGatt.writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray
): Result<Unit, BleException> {
return if(checkPermission()){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
}else{
characteristic.writeType
characteristic.value = data
writeCharacteristic(characteristic)
}
Result.success(Unit)
} else {
Result.failure(BleException.PermissionDenied)
}
}
fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_CONNECT) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.BLUETOOTH_SCAN) ==
PackageManager.PERMISSION_GRANTED
} else {
return ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(app, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
}
}
}

View File

@ -30,8 +30,6 @@ class WriteBeaconCallback(
) {
super.onConnectionStateChange(gatt, status, newState)
Log.d("beacon", "onConnectionStateChange $status $newState")
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
@ -56,7 +54,6 @@ class WriteBeaconCallback(
gatt: BluetoothGatt,
status: Int
) {
Log.d("beacon", "onServicesDiscovered $status")
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)

View File

@ -34,8 +34,6 @@ class WriteThermometerCallback(
) {
super.onConnectionStateChange(gatt, status, newState)
Log.d("th", "onConnectionStateChange $status $newState")
if(checkPermission()) {
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
@ -60,7 +58,6 @@ class WriteThermometerCallback(
gatt: BluetoothGatt,
status: Int
) {
Log.d("th", "onServicesDiscovered $status")
super.onServicesDiscovered(gatt, status)
onCycle(gatt, status)
@ -78,43 +75,37 @@ class WriteThermometerCallback(
(this shr (it * Byte.SIZE_BITS)).toByte()
}.toByteArray()
var uuid: Pair<UUID, ByteArray>? = null
var uuid: Triple<UUID, ByteArray, Ble.Thermometer.WriteRequest>? = null
uuid = request.historyInterval?.let {
this.request = request.copy(
historyInterval = null
)
Pair(
Triple(
intervalWriteUUID,
mutableListOf<Byte>(3).apply {
addAll((it).toUInt().to4ByteArrayInLittleEndian().reversed().toList())
}.toByteArray()
}.toByteArray(),
request.copy(
historyInterval = null
)
)
}
uuid = request.saveHistory?.let {
this.request = request.copy(
saveHistory = null
)
Pair(
Triple(
saveEnabledWriteUUID,
mutableListOf<Byte>(4).apply {
add(if (it) 1 else 0)
}.toByteArray()
}.toByteArray(),
request.copy(
saveHistory = null
)
)
} ?: uuid
uuid = request.tx?.let {
this.request = request.copy(
tx = null
)
Pair(
Triple(
txWriteUUID,
byteArrayOf(
when (it) {
@ -128,6 +119,9 @@ class WriteThermometerCallback(
Ble.BleState.TX.PLUS_3 -> 3
Ble.BleState.TX.PLUS_4 -> 4
}
),
request.copy(
tx = null
)
)
@ -140,7 +134,7 @@ class WriteThermometerCallback(
}?.let {
gatt.writeCharacteristic(it, uuid.second)
request = uuid.third
return
}
@ -183,8 +177,6 @@ class WriteThermometerCallback(
status: Int
) {
Log.d("th", "onCharacteristicWrite $status")
super.onCharacteristicWrite(gatt, characteristic, status)
if(checkPermission()) {

View File

@ -0,0 +1,99 @@
package llc.arma.ble.data
import android.R.attr.src
import android.app.Application
import android.icu.text.SimpleDateFormat
import android.os.Environment
import android.os.FileUtils
import llc.arma.ble.R
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.XlsxRepository
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.apache.poi.util.IOUtils
import org.apache.poi.xssf.usermodel.XSSFSheet
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.Date
import java.util.UUID
import javax.inject.Inject
class XlsxRepositoryImpl @Inject constructor(
private val application: Application
) : XlsxRepository {
override fun exportToXls(data: List<Ble.Accelerometer.HistoryPoint>): File {
val formatter = SimpleDateFormat("dd.MM.yyyy HH:mm")
val mailFile = File(application.filesDir, "${UUID.randomUUID()}.xlsx")
mailFile.createNewFile()
when(data.firstOrNull()){
is Ble.Accelerometer.HistoryPoint.Vibration -> {
IOUtils.copy(application.resources.openRawResource(R.raw.measure_single_axis), FileOutputStream(mailFile))
}
else -> {
IOUtils.copy(application.resources.openRawResource(R.raw.measure_multiple_axis), FileOutputStream(mailFile))
}
}
val fileIn = FileInputStream(mailFile)
val workbook = WorkbookFactory.create(fileIn)
val worksheet = workbook.getSheetAt(0) as XSSFSheet
data.withIndex().forEach {
val row = worksheet.createRow(it.index + 1)
val dateX = row.createCell(0)
val dateY = row.createCell(2)
val dateZ = row.createCell(4)
val x = row.createCell(1)
val y = row.createCell(3)
val z = row.createCell(5)
when(val value = it.value){
is Ble.Accelerometer.HistoryPoint.Angle -> {
dateX.setCellValue(formatter.format(Date(value.date)))
dateY.setCellValue(formatter.format(Date(value.date)))
dateZ.setCellValue(formatter.format(Date(value.date)))
x.setCellValue(value.x.toDouble())
y.setCellValue(value.y.toDouble())
z.setCellValue(value.z.toDouble())
}
is Ble.Accelerometer.HistoryPoint.Vibration -> {
dateX.setCellValue(formatter.format(Date(value.date)))
x.setCellValue(value.value.toDouble())
}
is Ble.Accelerometer.HistoryPoint.Accelerate -> {
dateX.setCellValue(formatter.format(Date(value.date)))
dateY.setCellValue(formatter.format(Date(value.date)))
dateZ.setCellValue(formatter.format(Date(value.date)))
x.setCellValue(value.x.toDouble())
y.setCellValue(value.y.toDouble())
z.setCellValue(value.z.toDouble())
}
}
}
fileIn.close()
val saveFos = FileOutputStream(mailFile)
workbook.write(saveFos)
saveFos.close()
val sharedFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "${UUID.randomUUID()}.xlsx")
sharedFile.createNewFile()
val sharedSaveFos = FileOutputStream(sharedFile)
workbook.write(sharedSaveFos)
sharedSaveFos.close()
workbook.close()
return mailFile
}
}

View File

@ -1,12 +1,68 @@
package llc.arma.ble.domain.model
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
sealed class Ble(
val info: BleInfo
) {
class Accelerometer(
info: BleInfo
): Ble(info){
info: BleInfo,
val state: BleState,
val accelerometerState: AccelerometerState
): Ble(info) {
sealed class History {
data class Enabled(
val scale: AccelScale,
val mode: AccelViewMode,
val detailed: Boolean
) : History()
object Disabled : History()
}
data class WriteRequest(
val tx: BleState.TX?,
val saveHistory: History?,
val historyInterval: Long?
)
sealed class HistoryPoint {
class Vibration (
val date: Long,
val value: Float
) : HistoryPoint()
class Accelerate (
val date: Long,
val x: Float,
val y: Float,
val z: Float
) : HistoryPoint()
class Angle (
val date: Long,
val x: Float,
val y: Float,
val z: Float
) : HistoryPoint()
}
class MeasurePoint (
val frequency: Long,
val value: Float
)
data class AccelerometerState(
val saveHistory: History,
val historyInterval: Long
)
}

View File

@ -6,7 +6,9 @@ data class BleInfo(
val batteryLevel: Int,
val rssi: Int?,
val type: Type,
val scanTime: Long
val scanTime: Long,
val tx: Int,
val recordEnabled: Boolean
){
enum class Type {

View File

@ -7,7 +7,12 @@ import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.MeasureData
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
interface BleRepository {
@ -23,7 +28,28 @@ interface BleRepository {
suspend fun writeBle(serial: String, request: Ble.Beacon.WriteRequest): Result<Unit, BleException>
suspend fun writeBle(serial: String, request: Ble.Accelerometer.WriteRequest): Result<Unit, BleException>
suspend fun changeBlePassword(password: String, serial: String): Result<Unit, BleException>
fun getAccelerometerMeasureBySerialFlow(serial: String): Flow<Result<Float, BleException>>
fun getAccelerometerMeasureBySerialFlow(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
): Flow<Result<MeasureData, BleException>>
suspend fun getAccelerometerSpectreBySerial(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>>
suspend fun getAccelerometerHistoryBySerial(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>>
}

View File

@ -0,0 +1,9 @@
package llc.arma.ble.domain.repository
import java.io.File
interface EmailRepository {
fun sendFile(file: File)
}

View File

@ -0,0 +1,11 @@
package llc.arma.ble.domain.repository
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.MeasureData
import java.io.File
interface XlsxRepository {
fun exportToXls(data: List<Ble.Accelerometer.HistoryPoint>): File
}

View File

@ -0,0 +1,34 @@
package llc.arma.ble.domain.usecase
import android.app.Application
import llc.arma.ble.R
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.EmailRepository
import llc.arma.ble.domain.repository.XlsxRepository
import org.apache.poi.ss.SpreadsheetVersion
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.apache.poi.ss.util.AreaReference
import org.apache.poi.ss.util.CellReference
import org.apache.poi.util.IOUtils
import org.apache.poi.xssf.usermodel.XSSFCell
import org.apache.poi.xssf.usermodel.XSSFSheet
import org.apache.poi.xssf.usermodel.XSSFTable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.UUID
import javax.inject.Inject
class ExportToXlsx @Inject constructor(
private val xlsxRepository: XlsxRepository,
private val emailRepository: EmailRepository
) {
operator fun invoke(data: List<Ble.Accelerometer.HistoryPoint>){
val file = xlsxRepository.exportToXls(data)
emailRepository.sendFile(file)
}
}

View File

@ -0,0 +1,21 @@
package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject
class GetAccelerometerHistoryBySerial @Inject constructor(
private val bleRepository: BleRepository
) {
suspend operator fun invoke(serial: String): Flow<Result<ProgressState<List<Ble.Accelerometer.HistoryPoint>>, BleException>> {
return bleRepository.getAccelerometerHistoryBySerial(serial)
}
}

View File

@ -3,8 +3,6 @@ package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject
@ -12,10 +10,54 @@ class GetAccelerometerMeasureBySerialFlow @Inject constructor(
private val bleRepository: BleRepository
) {
operator fun invoke(serial: String): Flow<Result<Float, BleException>> {
operator fun invoke(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
): Flow<Result<MeasureData, BleException>> {
return bleRepository.getAccelerometerMeasureBySerialFlow(serial)
return bleRepository.getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency)
}
}
enum class AccelViewMode {
ACCELERATION, PEAK_ACCELERATION, RMS, VIBRATION, ANGLE
}
enum class AccelScale(val k: Int) {
S_2(2_000), S_4(4_000), S_8(8_000), S_16(16_000)
}
sealed class MeasureData {
data class Angle(
val xAngle: Float,
val yAngle: Float,
val zAngle: Float,
val xAccelerate: Float,
val yAccelerate: Float,
val zAccelerate: Float,
) : MeasureData()
data class Accelerate(
val x: Float,
val y: Float,
val z: Float,
) : MeasureData()
data class Vibration(
val value: Float
) : MeasureData()
}
data class Accelerate(
val x: Float,
val y: Float,
val z: Float,
)

View File

@ -0,0 +1,40 @@
package llc.arma.ble.domain.usecase
import kotlinx.coroutines.flow.Flow
import llc.arma.ble.domain.Result
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.repository.BleRepository
import javax.inject.Inject
class GetAccelerometerSpectreBySerial @Inject constructor(
private val bleRepository: BleRepository
) {
suspend operator fun invoke(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency
): Flow<Result<ProgressState<List<Ble.Accelerometer.MeasurePoint>>, BleException>> {
return bleRepository.getAccelerometerSpectreBySerial(serial, accelScale, accelMode, fftAxis, fftMode, frequency)
}
}
enum class FftFrequency {
OFF, F_1, F_10, F_25, F_50, F_100, F_200, F_400, F_1620, F_1344
}
enum class FftViewMode {
SPECTRE, X, Y, Z
}
enum class FftAxis {
AUTO, X, Y, Z
}

View File

@ -24,4 +24,11 @@ class WriteBle @Inject constructor(
return bleRepository.writeBle(serial, request)
}
suspend operator fun invoke(
serial: String,
request: Ble.Accelerometer.WriteRequest
): llc.arma.ble.domain.Result<Unit, BleException>{
return bleRepository.writeBle(serial, request)
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="xlsx" path="/"/>
</paths>

View File

@ -14,7 +14,7 @@ buildscript {
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
}