Compare commits
4 Commits
HEAD
..
05af012479
| 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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<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">
|
<component name="ProjectType">
|
||||||
<option name="id" value="Android" />
|
<option name="id" value="Android" />
|
||||||
</component>
|
</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:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@drawable/variable_axle_load_icon"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/variable_axle_load_icon"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.VariableAxleMonitor">
|
android:theme="@style/Theme.VariableAxleMonitor">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -16,14 +17,21 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -35,11 +43,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.Dp
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.NavHost
|
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.ui.theme.VariableAxleMonitorTheme
|
||||||
import com.example.variableaxlemonitor.viewmodel.AxleViewModel
|
import com.example.variableaxlemonitor.viewmodel.AxleViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
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 {
|
VariableAxleMonitorTheme {
|
||||||
MainApp()
|
MainApp()
|
||||||
}
|
}
|
||||||
@@ -103,11 +79,44 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun MainApp() {
|
fun MainApp() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val viewModel: AxleViewModel = viewModel()
|
val viewModel: AxleViewModel = viewModel()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
NavHost(navController = navController, startDestination = "splash") {
|
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
composable("splash") {
|
arrayOf(
|
||||||
SplashScreen(navController)
|
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") {
|
composable("login") {
|
||||||
LoginScreen(navController, viewModel)
|
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
|
@Composable
|
||||||
fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
||||||
var user by remember { mutableStateOf("") }
|
var user by remember { mutableStateOf("") }
|
||||||
var pass by remember { mutableStateOf("") }
|
var pass by remember { mutableStateOf("") }
|
||||||
|
val context = LocalContext.current
|
||||||
val loginSuccess by viewModel.loginSuccess
|
val loginSuccess by viewModel.loginSuccess
|
||||||
|
|
||||||
LaunchedEffect(loginSuccess) {
|
LaunchedEffect(loginSuccess) {
|
||||||
if (loginSuccess) {
|
if (loginSuccess) {
|
||||||
navController.navigate("monitoring") {
|
navController.navigate("monitoring")
|
||||||
popUpTo("login") { inclusive = true }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loginError.collect { error ->
|
viewModel.loginError.collect { error ->
|
||||||
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
|
||||||
@@ -162,19 +146,20 @@ fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.Center,
|
.fillMaxSize()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
.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))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = user,
|
value = user,
|
||||||
onValueChange = { user = it },
|
onValueChange = { user = it },
|
||||||
label = { Text("ID") },
|
label = { Text("User") },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = pass,
|
value = pass,
|
||||||
onValueChange = { pass = it },
|
onValueChange = { pass = it },
|
||||||
@@ -182,82 +167,63 @@ fun LoginScreen(navController: NavController, viewModel: AxleViewModel) {
|
|||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.login(user, pass) },
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
@Composable
|
@Composable
|
||||||
fun MonitoringScreen(viewModel: AxleViewModel) {
|
fun MonitoringScreen(viewModel: AxleViewModel) {
|
||||||
val axleData by viewModel.axleData.collectAsState()
|
val axleData by viewModel.axleData.collectAsState()
|
||||||
val isConnected by viewModel.isConnected.collectAsState()
|
val isConnected by viewModel.isConnected.collectAsState()
|
||||||
val dataTrigger = viewModel.dataReceivedTrigger
|
|
||||||
var showDeviceDialog by remember { mutableStateOf(false) }
|
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
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text("가변축 시스템 모니터링", fontSize = 32.sp, fontWeight = FontWeight.Bold)
|
||||||
text = "가변축 모니터링",
|
|
||||||
fontSize = 32.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
Box(
|
IconButton(onClick = { showDeviceDialog = true }) {
|
||||||
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)
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Bluetooth,
|
imageVector = Icons.Default.Bluetooth,
|
||||||
contentDescription = "Bluetooth Status",
|
contentDescription = "Bluetooth Status",
|
||||||
tint = if (isConnected) Color.Green else Color.Red,
|
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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,73 +236,68 @@ fun MonitoringScreen(viewModel: AxleViewModel) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
LazyVerticalGrid(
|
||||||
// Row 1: 압력센서, 속도
|
columns = GridCells.Fixed(2),
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = PaddingValues(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
// 1. 압력센서
|
||||||
|
item {
|
||||||
MonitoringCard(
|
MonitoringCard(
|
||||||
title = "압력 센서",
|
title = "압력 센서",
|
||||||
value = String.format(Locale.US, "%.1f kgf/cm²", axleData.pressure * 0.1f),
|
value = String.format(Locale.US, "%.1f kgf/cm²", axleData.pressure * 0.1f),
|
||||||
iconRes = R.drawable.pressure_sensor_gauge,
|
iconRes = R.drawable.pressure_sensor,
|
||||||
isBitmap = true,
|
isBitmap = true
|
||||||
iconSize = 150.dp,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
customIcon = { PressureGauge(pressure = axleData.pressure * 0.1f) }
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
// 2. 속도
|
||||||
|
item {
|
||||||
MonitoringCard(
|
MonitoringCard(
|
||||||
title = "속도(Speed)",
|
title = "속도(Speed)",
|
||||||
value = "${axleData.speed} km/h",
|
value = "${axleData.speed} km/h",
|
||||||
iconRes = R.drawable.speed_display,
|
iconRes = R.drawable.speed_display,
|
||||||
isBitmap = true,
|
isBitmap = true
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
customIcon = { SpeedGauge(speed = axleData.speed) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 3. 가변축 하중
|
||||||
// Row 2: 가변축 하중, 구동축 하중
|
item {
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
|
||||||
MonitoringCard(
|
MonitoringCard(
|
||||||
title = "가변축 하중",
|
title = "가변축 하중",
|
||||||
value = String.format(Locale.US, "%.3f ton", axleData.variableAxleLoad * 0.001f),
|
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,
|
isBitmap = true,
|
||||||
iconSize = 200.dp,
|
iconSize = 60.dp
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
// 4. 구동축 하중
|
||||||
|
item {
|
||||||
MonitoringCard(
|
MonitoringCard(
|
||||||
title = "구동축 하중",
|
title = "구동축 하중",
|
||||||
value = String.format(Locale.US, "%.3f ton", axleData.driveAxleLoad * 0.001f),
|
value = String.format(Locale.US, "%.3f ton", axleData.driveAxleLoad * 0.001f),
|
||||||
iconRes = R.drawable.axle_load_driver,
|
iconRes = R.drawable.ic_axle_drive
|
||||||
isBitmap = true,
|
|
||||||
iconSize = 200.dp,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 5. 가변축 상태
|
||||||
// Row 3: 가변축 상태, 하중분배율
|
item {
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
|
||||||
MonitoringCard(
|
MonitoringCard(
|
||||||
title = "가변축 상태",
|
title = "가변축 상태",
|
||||||
value = if (axleData.isDown) "가변축 Down" else "가변축 Up",
|
value = if (axleData.isDown) "가변축 Down" else "가변축 Up",
|
||||||
iconRes = if (axleData.isDown) R.drawable.axle_down else R.drawable.axle_up,
|
iconRes = if (axleData.isDown) R.drawable.ic_axle_down else R.drawable.ic_axle_up
|
||||||
isBitmap = true,
|
|
||||||
iconSize = 200.dp,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
val distributionRatio = if (axleData.driveAxleLoad + axleData.variableAxleLoad > 0) {
|
}
|
||||||
(axleData.variableAxleLoad.toFloat() / (axleData.driveAxleLoad.toFloat()+axleData.variableAxleLoad.toFloat()) * 100f)
|
// 6. 설정 및 종료
|
||||||
} else 0f
|
item {
|
||||||
|
SystemCard(
|
||||||
MonitoringCard(
|
isConnected = isConnected,
|
||||||
title = "가변축 하중 분배율",
|
onBluetoothClick = { showDeviceDialog = true },
|
||||||
value = String.format(Locale.US, "%.1f %%", distributionRatio),
|
onExitClick = {
|
||||||
iconRes = when {
|
viewModel.disconnect()
|
||||||
distributionRatio < 50f -> R.drawable.load_sharing_ratio_d50
|
(context as? Activity)?.finishAffinity()
|
||||||
distributionRatio == 50f -> R.drawable.load_sharing_ratio_50
|
|
||||||
else -> R.drawable.load_sharing_ratio_u50
|
|
||||||
},
|
},
|
||||||
isBitmap = true,
|
dataTrigger = viewModel.dataReceivedTrigger,
|
||||||
iconSize = 300.dp,
|
modifier = Modifier.fillMaxWidth().aspectRatio(1.2f)
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,190 +306,51 @@ fun MonitoringScreen(viewModel: AxleViewModel) {
|
|||||||
Text(
|
Text(
|
||||||
text = "마지막 데이터 수신: $lastUpdate",
|
text = "마지막 데이터 수신: $lastUpdate",
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
fontSize = 11.sp,
|
fontSize = 12.sp,
|
||||||
color = Color.Gray
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDeviceDialog) {
|
if (showDeviceDialog) {
|
||||||
LaunchedEffect(Unit) {
|
val devices by viewModel.foundDevices.collectAsState()
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
viewModel.startScan()
|
viewModel.startScan()
|
||||||
|
onDispose {
|
||||||
|
viewModel.stopScan()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val foundDevices by viewModel.foundDevices.collectAsState()
|
|
||||||
BluetoothDeviceDialog(
|
BluetoothDeviceDialog(
|
||||||
devices = foundDevices,
|
devices = devices,
|
||||||
onDeviceSelected = { device ->
|
onDeviceSelected = {
|
||||||
viewModel.stopScan()
|
viewModel.connectToDevice(it)
|
||||||
viewModel.connectToDevice(device)
|
|
||||||
showDeviceDialog = false
|
showDeviceDialog = false
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = { showDeviceDialog = false }
|
||||||
viewModel.stopScan()
|
|
||||||
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
|
@Composable
|
||||||
fun MonitoringCard(
|
fun MonitoringCard(
|
||||||
modifier: Modifier = Modifier.fillMaxSize(),
|
|
||||||
title: String,
|
title: String,
|
||||||
value: String,
|
value: String,
|
||||||
iconRes: Int = 0,
|
iconRes: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1.2f), // Slightly wider to accommodate text
|
||||||
isBitmap: Boolean = false,
|
isBitmap: Boolean = false,
|
||||||
iconSize: Dp = 200.dp,
|
iconSize: Dp = 50.dp, // Reduced default size
|
||||||
titleFontSize: TextUnit = 32.sp,
|
titleFontSize: TextUnit = 18.sp, // Reduced font size
|
||||||
valueFontSize: TextUnit = 26.sp,
|
valueFontSize: TextUnit = 22.sp, // Reduced font size
|
||||||
titleModifier: Modifier = Modifier,
|
titleModifier: Modifier = Modifier,
|
||||||
valueModifier: Modifier = Modifier,
|
valueModifier: Modifier = Modifier,
|
||||||
iconModifier: Modifier = Modifier,
|
iconModifier: Modifier = Modifier
|
||||||
titlePaddingTop: Dp = 24.dp,
|
|
||||||
contentSpacing: Dp = 14.dp,
|
|
||||||
customIcon: @Composable (() -> Unit)? = null
|
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.padding(4.dp),
|
modifier = modifier,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
border = BorderStroke(1.dp, Color.LightGray)
|
border = BorderStroke(1.dp, Color.LightGray)
|
||||||
@@ -536,32 +358,18 @@ fun MonitoringCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = 8.dp),
|
.padding(8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(titlePaddingTop))
|
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
fontSize = titleFontSize,
|
fontSize = titleFontSize,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color(0xFF444444),
|
color = Color.DarkGray,
|
||||||
modifier = titleModifier
|
modifier = titleModifier
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(contentSpacing))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = valueFontSize,
|
|
||||||
fontWeight = FontWeight.ExtraBold,
|
|
||||||
color = Color.Blue,
|
|
||||||
modifier = valueModifier
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
if (customIcon != null) {
|
|
||||||
customIcon()
|
|
||||||
} else if (iconRes != 0) {
|
|
||||||
if (isBitmap) {
|
if (isBitmap) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = iconRes),
|
painter = painterResource(id = iconRes),
|
||||||
@@ -576,8 +384,84 @@ fun MonitoringCard(
|
|||||||
tint = MaterialTheme.colorScheme.secondary
|
tint = MaterialTheme.colorScheme.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
fontSize = valueFontSize,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
color = Color.Blue,
|
||||||
|
modifier = valueModifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
) {
|
||||||
|
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 ->
|
items(devices) { device ->
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
val deviceName = device.name ?: "알 수 없는 기기"
|
val deviceName = device.name ?: "알 수 없는 기기"
|
||||||
Text(
|
val deviceAddress = device.address
|
||||||
text = deviceName,
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onDeviceSelected(device) }
|
.clickable { onDeviceSelected(device) }
|
||||||
.padding(vertical = 12.dp),
|
.padding(vertical = 12.dp)
|
||||||
fontSize = 16.sp
|
) {
|
||||||
)
|
Text(text = deviceName, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text(text = deviceAddress, fontSize = 12.sp, color = Color.Gray)
|
||||||
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (devices.isEmpty()) {
|
if (devices.isEmpty()) {
|
||||||
Text("검색된 기기가 없습니다.", modifier = Modifier.padding(vertical = 16.dp))
|
Text("주변 기기를 검색 중입니다...", modifier = Modifier.padding(vertical = 16.dp))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
TextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.End)) {
|
TextButton(onClick = onDismiss, modifier = Modifier.align(Alignment.End)) {
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ class BluetoothManager(private val context: Context) {
|
|||||||
val foundDevices: StateFlow<List<BluetoothDevice>> = _foundDevices
|
val foundDevices: StateFlow<List<BluetoothDevice>> = _foundDevices
|
||||||
|
|
||||||
private var gatt: BluetoothGatt? = null
|
private var gatt: BluetoothGatt? = null
|
||||||
|
private val dataBuffer = StringBuilder()
|
||||||
|
|
||||||
private val scanCallback = object : ScanCallback() {
|
private val scanCallback = object : ScanCallback() {
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
val device = result.device
|
val device = result.device
|
||||||
// BLE 기기만 필터링 (DEVICE_TYPE_LE 또는 DEVICE_TYPE_DUAL)
|
if (device.name != null) {
|
||||||
if (device.name != null && (device.type == BluetoothDevice.DEVICE_TYPE_LE || device.type == BluetoothDevice.DEVICE_TYPE_DUAL)) {
|
|
||||||
val currentList = _foundDevices.value
|
val currentList = _foundDevices.value
|
||||||
if (currentList.none { it.address == device.address }) {
|
if (currentList.none { it.address == device.address }) {
|
||||||
_foundDevices.value = currentList + device
|
_foundDevices.value = currentList + device
|
||||||
@@ -167,11 +167,42 @@ class BluetoothManager(private val context: Context) {
|
|||||||
private fun processReceivedData(value: ByteArray?) {
|
private fun processReceivedData(value: ByteArray?) {
|
||||||
if (value == null) return
|
if (value == null) return
|
||||||
|
|
||||||
val received = String(value).trim()
|
val received = String(value)
|
||||||
if (received.isNotEmpty()) {
|
// Log.d(TAG, "Raw Data (String): $received")
|
||||||
Log.d(TAG, "Extracted line: '$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 {
|
scope.launch {
|
||||||
parseData(received)
|
parseData(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,12 +231,7 @@ class BluetoothManager(private val context: Context) {
|
|||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun startScan() {
|
fun startScan() {
|
||||||
Log.d(TAG, "Starting BLE scan...")
|
Log.d(TAG, "Starting BLE scan...")
|
||||||
// 페어링된 기기 중 BLE 기기만 먼저 추가
|
_foundDevices.value = emptyList()
|
||||||
val pairedDevices = adapter?.bondedDevices ?: emptySet()
|
|
||||||
_foundDevices.value = pairedDevices.filter {
|
|
||||||
it.type == BluetoothDevice.DEVICE_TYPE_LE || it.type == BluetoothDevice.DEVICE_TYPE_DUAL
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner?.startScan(scanCallback)
|
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 |