Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05af012479 | |||
| 86868d2b8e | |||
| f601964083 | |||
| e23b30605f |
@@ -0,0 +1,123 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="temurin-25" project-jdk-type="JavaSDK" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="temurin-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PlanningModeManager">
|
||||
<option name="approvalStates">
|
||||
<map>
|
||||
<entry key="b43a2b32-8f18-49df-9a9e-f3eeb7b4a739" value="false" />
|
||||
<entry key="c04a9953-a5f1-4945-b3ad-be57a23dba29" value="false" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -13,9 +13,9 @@
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/variable_axle_load_icon"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/variable_axle_load_icon"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.VariableAxleMonitor">
|
||||
<activity
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
@@ -16,14 +17,21 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -35,11 +43,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -49,6 +53,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.compose.NavHost
|
||||
@@ -57,41 +62,12 @@ import androidx.navigation.compose.rememberNavController
|
||||
import com.example.variableaxlemonitor.ui.theme.VariableAxleMonitorTheme
|
||||
import com.example.variableaxlemonitor.viewmodel.AxleViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
listOf(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { results ->
|
||||
if (results.values.all { it }) {
|
||||
Log.d("MainActivity", "All permissions granted")
|
||||
} else {
|
||||
Toast.makeText(this, "블루투스 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launcher.launch(permissions.toTypedArray())
|
||||
}
|
||||
|
||||
VariableAxleMonitorTheme {
|
||||
MainApp()
|
||||
}
|
||||
@@ -103,11 +79,44 @@ class MainActivity : ComponentActivity() {
|
||||
fun MainApp() {
|
||||
val navController = rememberNavController()
|
||||
val viewModel: AxleViewModel = viewModel()
|
||||
val context = LocalContext.current
|
||||
|
||||
NavHost(navController = navController, startDestination = "splash") {
|
||||
composable("splash") {
|
||||
SplashScreen(navController)
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
} else {
|
||||
arrayOf(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { results ->
|
||||
if (results.values.all { it }) {
|
||||
// Permissions granted
|
||||
} else {
|
||||
Toast.makeText(context, "Permissions required for Bluetooth", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val missing = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (missing.isNotEmpty()) {
|
||||
launcher.launch(missing.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
NavHost(navController = navController, startDestination = "login") {
|
||||
composable("login") {
|
||||
LoginScreen(navController, viewModel)
|
||||
}
|
||||
@@ -117,44 +126,19 @@ fun MainApp() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(navController: NavController) {
|
||||
LaunchedEffect(Unit) {
|
||||
delay(2000) // 2초 대기
|
||||
navController.navigate("login") {
|
||||
popUpTo("splash") { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.variable_axle_load_loading),
|
||||
contentDescription = "App Title",
|
||||
modifier = Modifier.fillMaxWidth(0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
||||
var user by remember { mutableStateOf("") }
|
||||
var pass by remember { mutableStateOf("") }
|
||||
val context = LocalContext.current
|
||||
val loginSuccess by viewModel.loginSuccess
|
||||
|
||||
LaunchedEffect(loginSuccess) {
|
||||
if (loginSuccess) {
|
||||
navController.navigate("monitoring") {
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
navController.navigate("monitoring")
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loginError.collect { error ->
|
||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||
@@ -162,19 +146,20 @@ fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("Variable Axle Monitor", fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
||||
Text("가변축 시스템", fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
OutlinedTextField(
|
||||
value = user,
|
||||
onValueChange = { user = it },
|
||||
label = { Text("ID") },
|
||||
label = { Text("User") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = pass,
|
||||
onValueChange = { pass = it },
|
||||
@@ -182,82 +167,63 @@ fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = { viewModel.login(user, pass) },
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp)
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Login")
|
||||
Text("로그인")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TextButton(
|
||||
onClick = { (context as? Activity)?.finishAffinity() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("앱 종료")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Composable
|
||||
fun MonitoringScreen(viewModel: AxleViewModel) {
|
||||
val axleData by viewModel.axleData.collectAsState()
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
val dataTrigger = viewModel.dataReceivedTrigger
|
||||
var showDeviceDialog by remember { mutableStateOf(false) }
|
||||
val lastUpdate by remember(axleData.timestamp) {
|
||||
derivedStateOf {
|
||||
if (axleData.timestamp == 0L) "--:--:--"
|
||||
else SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(axleData.timestamp))
|
||||
}
|
||||
}
|
||||
var isBlinking by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(dataTrigger) {
|
||||
dataTrigger.collect {
|
||||
isBlinking = true
|
||||
delay(200)
|
||||
isBlinking = false
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
var lastUpdate by remember { mutableStateOf("-") }
|
||||
var receiveCount by remember { mutableIntStateOf(0) }
|
||||
val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
Log.d("MonitoringScreen", "ViewModel Instance: ${System.identityHashCode(viewModel)}")
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.dataReceivedTrigger.collect {
|
||||
receiveCount++
|
||||
lastUpdate = timeFormatter.format(Date())
|
||||
Log.d("MonitoringScreen", "Data Received Trigger #$receiveCount at $lastUpdate")
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(axleData) {
|
||||
Log.d("MonitoringScreen", "UI Update: $axleData")
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "가변축 모니터링",
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text("가변축 시스템 모니터링", fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
||||
},
|
||||
actions = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isBlinking) Color.Green else Color.LightGray.copy(alpha = 0.3f))
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
IconButton(
|
||||
onClick = { showDeviceDialog = true },
|
||||
modifier = Modifier.size(64.dp)
|
||||
) {
|
||||
IconButton(onClick = { showDeviceDialog = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = "Bluetooth Status",
|
||||
tint = if (isConnected) Color.Green else Color.Red,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
viewModel.disconnect()
|
||||
(context as? Activity)?.finishAffinity()
|
||||
},
|
||||
modifier = Modifier.size(64.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExitToApp,
|
||||
contentDescription = "Exit App",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(48.dp)
|
||||
tint = if (isConnected) Color.Green else Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -270,73 +236,68 @@ fun MonitoringScreen(viewModel: AxleViewModel) {
|
||||
.fillMaxSize()
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
// Row 1: 압력센서, 속도
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
// 1. 압력센서
|
||||
item {
|
||||
MonitoringCard(
|
||||
title = "압력 센서",
|
||||
value = String.format(Locale.US, "%.1f kgf/cm²", axleData.pressure * 0.1f),
|
||||
iconRes = R.drawable.pressure_sensor_gauge,
|
||||
isBitmap = true,
|
||||
iconSize = 150.dp,
|
||||
modifier = Modifier.weight(1f),
|
||||
customIcon = { PressureGauge(pressure = axleData.pressure * 0.1f) }
|
||||
iconRes = R.drawable.pressure_sensor,
|
||||
isBitmap = true
|
||||
)
|
||||
}
|
||||
// 2. 속도
|
||||
item {
|
||||
MonitoringCard(
|
||||
title = "속도(Speed)",
|
||||
value = "${axleData.speed} km/h",
|
||||
iconRes = R.drawable.speed_display,
|
||||
isBitmap = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
customIcon = { SpeedGauge(speed = axleData.speed) }
|
||||
isBitmap = true
|
||||
)
|
||||
}
|
||||
|
||||
// Row 2: 가변축 하중, 구동축 하중
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// 3. 가변축 하중
|
||||
item {
|
||||
MonitoringCard(
|
||||
title = "가변축 하중",
|
||||
value = String.format(Locale.US, "%.3f ton", axleData.variableAxleLoad * 0.001f),
|
||||
iconRes = R.drawable.axle_load_variable,
|
||||
iconRes = R.drawable.variable_axle_load,
|
||||
isBitmap = true,
|
||||
iconSize = 200.dp,
|
||||
modifier = Modifier.weight(1f)
|
||||
iconSize = 60.dp
|
||||
)
|
||||
}
|
||||
// 4. 구동축 하중
|
||||
item {
|
||||
MonitoringCard(
|
||||
title = "구동축 하중",
|
||||
value = String.format(Locale.US, "%.3f ton", axleData.driveAxleLoad * 0.001f),
|
||||
iconRes = R.drawable.axle_load_driver,
|
||||
isBitmap = true,
|
||||
iconSize = 200.dp,
|
||||
modifier = Modifier.weight(1f)
|
||||
iconRes = R.drawable.ic_axle_drive
|
||||
)
|
||||
}
|
||||
|
||||
// Row 3: 가변축 상태, 하중분배율
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// 5. 가변축 상태
|
||||
item {
|
||||
MonitoringCard(
|
||||
title = "가변축 상태",
|
||||
value = if (axleData.isDown) "가변축 Down" else "가변축 Up",
|
||||
iconRes = if (axleData.isDown) R.drawable.axle_down else R.drawable.axle_up,
|
||||
isBitmap = true,
|
||||
iconSize = 200.dp,
|
||||
modifier = Modifier.weight(1f)
|
||||
iconRes = if (axleData.isDown) R.drawable.ic_axle_down else R.drawable.ic_axle_up
|
||||
)
|
||||
val distributionRatio = if (axleData.driveAxleLoad + axleData.variableAxleLoad > 0) {
|
||||
(axleData.variableAxleLoad.toFloat() / (axleData.driveAxleLoad.toFloat()+axleData.variableAxleLoad.toFloat()) * 100f)
|
||||
} else 0f
|
||||
|
||||
MonitoringCard(
|
||||
title = "가변축 하중 분배율",
|
||||
value = String.format(Locale.US, "%.1f %%", distributionRatio),
|
||||
iconRes = when {
|
||||
distributionRatio < 50f -> R.drawable.load_sharing_ratio_d50
|
||||
distributionRatio == 50f -> R.drawable.load_sharing_ratio_50
|
||||
else -> R.drawable.load_sharing_ratio_u50
|
||||
}
|
||||
// 6. 설정 및 종료
|
||||
item {
|
||||
SystemCard(
|
||||
isConnected = isConnected,
|
||||
onBluetoothClick = { showDeviceDialog = true },
|
||||
onExitClick = {
|
||||
viewModel.disconnect()
|
||||
(context as? Activity)?.finishAffinity()
|
||||
},
|
||||
isBitmap = true,
|
||||
iconSize = 300.dp,
|
||||
modifier = Modifier.weight(1f)
|
||||
dataTrigger = viewModel.dataReceivedTrigger,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1.2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -345,190 +306,51 @@ fun MonitoringScreen(viewModel: AxleViewModel) {
|
||||
Text(
|
||||
text = "마지막 데이터 수신: $lastUpdate",
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
fontSize = 11.sp,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDeviceDialog) {
|
||||
LaunchedEffect(Unit) {
|
||||
val devices by viewModel.foundDevices.collectAsState()
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
viewModel.startScan()
|
||||
onDispose {
|
||||
viewModel.stopScan()
|
||||
}
|
||||
}
|
||||
|
||||
val foundDevices by viewModel.foundDevices.collectAsState()
|
||||
BluetoothDeviceDialog(
|
||||
devices = foundDevices,
|
||||
onDeviceSelected = { device ->
|
||||
viewModel.stopScan()
|
||||
viewModel.connectToDevice(device)
|
||||
devices = devices,
|
||||
onDeviceSelected = {
|
||||
viewModel.connectToDevice(it)
|
||||
showDeviceDialog = false
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.stopScan()
|
||||
showDeviceDialog = false
|
||||
}
|
||||
onDismiss = { showDeviceDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PressureGauge(
|
||||
pressure: Float,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val maxPressure = 12f
|
||||
// 사용자의 요청에 따라 0은 135도, 12은 -135도로 설정
|
||||
val startAngle = 136f //135f
|
||||
val sweepAngle = 270f //270f
|
||||
|
||||
val animatedPressure by animateFloatAsState(
|
||||
targetValue = pressure.coerceIn(0f, maxPressure),
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
label = "PressureNeedle"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(0.6f) // 게이지 이미지를 부모 영역의 80%로 제한
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.pressure_sensor_gauge),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
val angleDeg = startAngle + (animatedPressure / maxPressure) * sweepAngle
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.pressure_arrow2),
|
||||
contentDescription = "Pressure Needle",
|
||||
modifier = Modifier
|
||||
.fillMaxSize(0.7f) //0.7
|
||||
.graphicsLayer(
|
||||
rotationZ = angleDeg + 90f,
|
||||
transformOrigin = TransformOrigin(0.5f, 0.5f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpeedGauge(
|
||||
speed: Int,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val maxSpeed = 160f
|
||||
// 사용자의 피드백에 따라 수평 왼쪽(180도)을 0으로, 수평 오른쪽(0도)을 160으로 설정
|
||||
val startAngle = 180f
|
||||
val sweepAngle = 180f
|
||||
|
||||
val animatedSpeed by animateFloatAsState(
|
||||
targetValue = speed.coerceIn(0, maxSpeed.toInt()).toFloat(),
|
||||
animationSpec = tween(durationMillis = 500),
|
||||
label = "SpeedNeedle"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.fillMaxSize(0.8f), // 전체 사이즈 0.8배 축소
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.speed_display),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
// 바늘의 회전 중심축 위치를 사용자가 확인한 0.7f 지점으로 고정
|
||||
val center = Offset(size.width / 2, size.height * 0.72f)
|
||||
val radius = size.minDimension / 2
|
||||
|
||||
// 시계 방향으로 회전하므로 각도 계산
|
||||
val angleDeg = startAngle + (animatedSpeed / maxSpeed) * sweepAngle
|
||||
val angleRad = Math.toRadians(angleDeg.toDouble())
|
||||
|
||||
// 바늘의 길이 (게이지 크기에 맞춰 조정)
|
||||
val needleLength = radius * 0.8f
|
||||
|
||||
// 바늘(화살표) 그리기 - 삼각형 형태
|
||||
val needlePath = androidx.compose.ui.graphics.Path().apply {
|
||||
val endX = center.x + needleLength * cos(angleRad).toFloat()
|
||||
val endY = center.y + needleLength * sin(angleRad).toFloat()
|
||||
|
||||
// 바늘 뿌리 부분의 각도 (바늘 방향과 수직)
|
||||
val baseAngleRad = Math.toRadians((angleDeg + 90).toDouble())
|
||||
val baseWidth = 8.dp.toPx() // 뿌리 너비
|
||||
|
||||
val baseLeftX = center.x + baseWidth * cos(baseAngleRad).toFloat()
|
||||
val baseLeftY = center.y + baseWidth * sin(baseAngleRad).toFloat()
|
||||
|
||||
val baseRightX = center.x - baseWidth * cos(baseAngleRad).toFloat()
|
||||
val baseRightY = center.y - baseWidth * sin(baseAngleRad).toFloat()
|
||||
|
||||
moveTo(baseLeftX, baseLeftY)
|
||||
lineTo(endX, endY)
|
||||
lineTo(baseRightX, baseRightY)
|
||||
close()
|
||||
}
|
||||
|
||||
// 바늘 그림자
|
||||
drawPath(
|
||||
path = needlePath,
|
||||
color = Color.Black.copy(alpha = 0.2f),
|
||||
style = androidx.compose.ui.graphics.drawscope.Fill
|
||||
)
|
||||
|
||||
// 바늘 본체 (빨간색)
|
||||
drawPath(
|
||||
path = needlePath,
|
||||
color = Color.Red,
|
||||
style = androidx.compose.ui.graphics.drawscope.Fill
|
||||
)
|
||||
|
||||
// 중앙 검은 점(캡) 그리기
|
||||
drawCircle(
|
||||
color = Color.DarkGray,
|
||||
radius = 10.dp.toPx(),
|
||||
center = center
|
||||
)
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
radius = 6.dp.toPx(),
|
||||
center = center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonitoringCard(
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
title: String,
|
||||
value: String,
|
||||
iconRes: Int = 0,
|
||||
iconRes: Int,
|
||||
modifier: Modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.2f), // Slightly wider to accommodate text
|
||||
isBitmap: Boolean = false,
|
||||
iconSize: Dp = 200.dp,
|
||||
titleFontSize: TextUnit = 32.sp,
|
||||
valueFontSize: TextUnit = 26.sp,
|
||||
iconSize: Dp = 50.dp, // Reduced default size
|
||||
titleFontSize: TextUnit = 18.sp, // Reduced font size
|
||||
valueFontSize: TextUnit = 22.sp, // Reduced font size
|
||||
titleModifier: Modifier = Modifier,
|
||||
valueModifier: Modifier = Modifier,
|
||||
iconModifier: Modifier = Modifier,
|
||||
titlePaddingTop: Dp = 24.dp,
|
||||
contentSpacing: Dp = 14.dp,
|
||||
customIcon: @Composable (() -> Unit)? = null
|
||||
iconModifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.padding(4.dp),
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color.LightGray)
|
||||
@@ -536,18 +358,33 @@ fun MonitoringCard(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(titlePaddingTop))
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = titleFontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF444444),
|
||||
color = Color.DarkGray,
|
||||
modifier = titleModifier
|
||||
)
|
||||
Spacer(modifier = Modifier.height(contentSpacing))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
if (isBitmap) {
|
||||
Image(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = title,
|
||||
modifier = iconModifier.size(iconSize)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = title,
|
||||
modifier = iconModifier.size(iconSize),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = value,
|
||||
fontSize = valueFontSize,
|
||||
@@ -555,28 +392,75 @@ fun MonitoringCard(
|
||||
color = Color.Blue,
|
||||
modifier = valueModifier
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SystemCard(
|
||||
isConnected: Boolean,
|
||||
onBluetoothClick: () -> Unit,
|
||||
onExitClick: () -> Unit,
|
||||
dataTrigger: kotlinx.coroutines.flow.SharedFlow<Unit>,
|
||||
modifier: Modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f),
|
||||
topSectionModifier: Modifier = Modifier,
|
||||
bottomSectionModifier: Modifier = Modifier
|
||||
) {
|
||||
var isBlinking by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
dataTrigger.collect {
|
||||
isBlinking = true
|
||||
delay(200)
|
||||
isBlinking = false
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color.DarkGray)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = topSectionModifier.fillMaxWidth()
|
||||
) {
|
||||
if (customIcon != null) {
|
||||
customIcon()
|
||||
} else if (iconRes != 0) {
|
||||
if (isBitmap) {
|
||||
Image(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = title,
|
||||
modifier = iconModifier.size(iconSize)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = title,
|
||||
modifier = iconModifier.size(iconSize),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = "Bluetooth",
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clickable { onBluetoothClick() },
|
||||
tint = if (isConnected) Color.Green else Color.Red
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (isBlinking) Color.Green else Color.Transparent)
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onExitClick,
|
||||
modifier = bottomSectionModifier,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error)
|
||||
) {
|
||||
Icon(Icons.Default.ExitToApp, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("종료")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,19 +484,21 @@ fun BluetoothDeviceDialog(
|
||||
items(devices) { device ->
|
||||
@SuppressLint("MissingPermission")
|
||||
val deviceName = device.name ?: "알 수 없는 기기"
|
||||
Text(
|
||||
text = deviceName,
|
||||
val deviceAddress = device.address
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onDeviceSelected(device) }
|
||||
.padding(vertical = 12.dp),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Text(text = deviceName, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(text = deviceAddress, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
if (devices.isEmpty()) {
|
||||
Text("검색된 기기가 없습니다.", modifier = Modifier.padding(vertical = 16.dp))
|
||||
Text("주변 기기를 검색 중입니다...", modifier = Modifier.padding(vertical = 16.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.End)) {
|
||||
|
||||
@@ -52,13 +52,13 @@ class BluetoothManager(private val context: Context) {
|
||||
val foundDevices: StateFlow<List<BluetoothDevice>> = _foundDevices
|
||||
|
||||
private var gatt: BluetoothGatt? = null
|
||||
private val dataBuffer = StringBuilder()
|
||||
|
||||
private val scanCallback = object : ScanCallback() {
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
val device = result.device
|
||||
// BLE 기기만 필터링 (DEVICE_TYPE_LE 또는 DEVICE_TYPE_DUAL)
|
||||
if (device.name != null && (device.type == BluetoothDevice.DEVICE_TYPE_LE || device.type == BluetoothDevice.DEVICE_TYPE_DUAL)) {
|
||||
if (device.name != null) {
|
||||
val currentList = _foundDevices.value
|
||||
if (currentList.none { it.address == device.address }) {
|
||||
_foundDevices.value = currentList + device
|
||||
@@ -167,11 +167,42 @@ class BluetoothManager(private val context: Context) {
|
||||
private fun processReceivedData(value: ByteArray?) {
|
||||
if (value == null) return
|
||||
|
||||
val received = String(value).trim()
|
||||
if (received.isNotEmpty()) {
|
||||
Log.d(TAG, "Extracted line: '$received'")
|
||||
scope.launch {
|
||||
parseData(received)
|
||||
val received = String(value)
|
||||
// Log.d(TAG, "Raw Data (String): $received")
|
||||
|
||||
synchronized(dataBuffer) {
|
||||
dataBuffer.append(received)
|
||||
|
||||
// Check for various delimiters including literal strings like "\r\n" or "\r\"
|
||||
val delimiters = listOf("\r\n", "\\r\\n", "\\r\\", "\n", "\r")
|
||||
|
||||
var foundDelimiter = true
|
||||
while (foundDelimiter) {
|
||||
foundDelimiter = false
|
||||
var earliestIndex = Int.MAX_VALUE
|
||||
var selectedDelimiter = ""
|
||||
|
||||
for (delim in delimiters) {
|
||||
val index = dataBuffer.indexOf(delim)
|
||||
if (index != -1 && index < earliestIndex) {
|
||||
earliestIndex = index
|
||||
selectedDelimiter = delim
|
||||
foundDelimiter = true
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDelimiter) {
|
||||
val line = dataBuffer.substring(0, earliestIndex).trim()
|
||||
dataBuffer.delete(0, earliestIndex + selectedDelimiter.length)
|
||||
|
||||
if (line.isNotEmpty()) {
|
||||
Log.d(TAG, "Extracted line: '$line'")
|
||||
// _lastMessage.value = "데이터 수집 중: $line"
|
||||
scope.launch {
|
||||
parseData(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,12 +231,7 @@ class BluetoothManager(private val context: Context) {
|
||||
@SuppressLint("MissingPermission")
|
||||
fun startScan() {
|
||||
Log.d(TAG, "Starting BLE scan...")
|
||||
// 페어링된 기기 중 BLE 기기만 먼저 추가
|
||||
val pairedDevices = adapter?.bondedDevices ?: emptySet()
|
||||
_foundDevices.value = pairedDevices.filter {
|
||||
it.type == BluetoothDevice.DEVICE_TYPE_LE || it.type == BluetoothDevice.DEVICE_TYPE_DUAL
|
||||
}
|
||||
|
||||
_foundDevices.value = emptyList()
|
||||
scanner?.startScan(scanCallback)
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 494 KiB |
|
Before Width: | Height: | Size: 388 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 561 KiB |
|
Before Width: | Height: | Size: 561 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 156 KiB |