diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index cf06e9b..9cefd2d 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -44,7 +44,7 @@ import com.example.medilightv2android.services.MeasurementLogService import com.example.medilightv2android.ui.theme.* /** - * Sensor Placement Guide -- shown after BLE connection, before measurement. + * Sensor Alignment Guide -- shown after BLE connection, before measurement. * * Displays a 6-channel sensor layout, shows per-channel UrinAI detection * (green = detected, red = not), and provides direction guidance. @@ -62,7 +62,7 @@ fun PlacementGuideView(appState: AppState) { var isContinuousMode by remember { mutableStateOf(true) } val channelDetected = remember { mutableStateListOf(false, false, false, false, false, false) } val channelUrineLen = remember { mutableStateListOf(0, 0, 0, 0, 0, 0) } - var directionHint by remember { mutableStateOf("센서를 부착해주세요") } + var directionHint by remember { mutableStateOf("Please attach the sensor") } var directionIcon by remember { mutableStateOf("") } var placementScore by remember { mutableIntStateOf(0) } var scanCount by remember { mutableIntStateOf(0) } @@ -88,7 +88,7 @@ fun PlacementGuideView(appState: AppState) { isLocked = false scanCount = 0 placementScore = 0 - directionHint = "센서를 부착해주세요" + directionHint = "Please attach the sensor" directionIcon = "" for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 } } @@ -126,7 +126,7 @@ fun PlacementGuideView(appState: AppState) { ) } Text( - text = "Sensor Placement", + text = "Sensor Alignment", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp) @@ -153,7 +153,7 @@ fun PlacementGuideView(appState: AppState) { Box( modifier = Modifier .fillMaxWidth() - .height(300.dp) + .height(500.dp) .padding(top = 8.dp), contentAlignment = Alignment.Center ) { @@ -174,9 +174,9 @@ fun PlacementGuideView(appState: AppState) { val greenZone = isGreenZone // 방광 위치 (이미지의 배꼽~치골 사이) - val bladderCy = h * 0.58f + val bladderRy = h * 0.05f val bladderRx = w * 0.12f - val bladderRy = h * 0.10f + val bladderCy = h * 0.48f + bladderRy // 위쪽 경계 고정 drawOval( color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f), topLeft = Offset(cx - bladderRx, bladderCy - bladderRy), @@ -189,55 +189,43 @@ fun PlacementGuideView(appState: AppState) { style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f))) ) - // 센서 기기 (둥근 사각형) - val sensorW = 48f - val sensorH = 64f + // 센서 위치 계산 (이미지 오버레이용) + val sensorW = 80f + val sensorH = 50f val sensorY = bladderCy - sensorH / 2 - val sensorColor = if (greenZone) Color(0xFF4CAF50) else MlPrimary - drawRoundRect( - color = sensorColor.copy(alpha = 0.2f), - topLeft = Offset(cx - sensorW / 2, sensorY), - size = Size(sensorW, sensorH), - cornerRadius = CornerRadius(12f) - ) - drawRoundRect( - color = sensorColor, - topLeft = Offset(cx - sensorW / 2, sensorY), - size = Size(sensorW, sensorH), - cornerRadius = CornerRadius(12f), - style = Stroke(width = 3f) - ) - // ── 방향 화살표 ── + // ── 방향 화살표 (타원 바깥에 크게) ── if (!greenZone) { - val arrowColor = Color(0xFFFF9800) - val arrowLen = 60f - val arrowHead = 18f + val arrowColor = Color(0xFFFF5722) + val arrowLen = 50f + val arrowHead = 20f + val sw = 7f + val gap = 12f // 타원 경계에서 간격 when { directionIcon.contains("up") -> { - val ax = cx; val ay = sensorY - 20f - drawLine(arrowColor, Offset(ax, ay + arrowLen), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f) + val ax = cx; val ay = bladderCy - bladderRy - gap + drawLine(arrowColor, Offset(ax, ay + arrowLen), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw) } directionIcon.contains("down") -> { - val ax = cx; val ay = sensorY + sensorH + 20f - drawLine(arrowColor, Offset(ax, ay - arrowLen), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f) + val ax = cx; val ay = bladderCy + bladderRy + gap + drawLine(arrowColor, Offset(ax, ay - arrowLen), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw) } directionIcon.contains("left") -> { - val ax = cx - sensorW / 2 - 25f; val ay = sensorY + sensorH / 2 - drawLine(arrowColor, Offset(ax + arrowLen, ay), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f) + val ax = cx - bladderRx - gap; val ay = bladderCy + drawLine(arrowColor, Offset(ax + arrowLen, ay), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw) } directionIcon.contains("right") -> { - val ax = cx + sensorW / 2 + 25f; val ay = sensorY + sensorH / 2 - drawLine(arrowColor, Offset(ax - arrowLen, ay), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f) - drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f) + val ax = cx + bladderRx + gap; val ay = bladderCy + drawLine(arrowColor, Offset(ax - arrowLen, ay), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw) + drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw) } } } @@ -255,6 +243,18 @@ fun PlacementGuideView(appState: AppState) { } } + // 센서 기기 이미지 (방광 위에 오버레이) + androidx.compose.foundation.Image( + painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device), + contentDescription = "Sensor", + modifier = Modifier + .size(width = 200.dp, height = 125.dp) + .align(Alignment.Center) + .offset(x = 5.dp, y = 20.dp), + contentScale = androidx.compose.ui.layout.ContentScale.Fit, + alpha = if (isGreenZone) 0.7f else 0.95f + ) + // Score + 감지수 (우측 상단) Column( modifier = Modifier @@ -479,7 +479,7 @@ fun PlacementGuideView(appState: AppState) { val detachResult = if (fullCh.size >= 6) DetachmentDetection.detect(buffers) else null if (detachResult != null && detachResult.isDetached) { - directionHint = "센서가 부착되지 않았습니다. 부착해주세요" + directionHint = "Sensor not attached. Please attach the sensor" directionIcon = "exclamationmark.triangle" placementScore = 0 if (lastLedMode != 4) { bleManager.sendLedMode(4); lastLedMode = 4 } @@ -561,7 +561,7 @@ fun PlacementGuideView(appState: AppState) { } } - // Placement Complete button + // Alignment Complete button Button( onClick = { isContinuousMode = false @@ -579,7 +579,7 @@ fun PlacementGuideView(appState: AppState) { ) { Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White) Spacer(modifier = Modifier.width(8.dp)) - Text("Placement Complete", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White) + Text("Alignment Complete", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White) } // Skip button @@ -904,7 +904,7 @@ private fun computePlacementGuide( val detectedCount = detected.count { it } if (detectedCount == 0) { - return PlacementGuideResult("방광이 감지되지 않습니다. 센서를 위로 천천히 올려보세요 ↑", "arrow.up", 0, + return PlacementGuideResult("Bladder not found. Gradually raise the sensor ↑", "arrow.up", 0, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -928,8 +928,8 @@ private fun computePlacementGuide( vs.stableAtMax = 0 val hint: String; val icon: String when { - (ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" } - else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" } + (ch3 || ch2) && !ch0 && !ch1 -> { hint = "Gradually lower the sensor ↓"; icon = "arrow.down" } + else -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" } } return PlacementGuideResult(hint, icon, centerDetected * 10, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) @@ -937,21 +937,21 @@ private fun computePlacementGuide( if (centerDetected >= vs.maxCenterSeen) { vs.maxCenterSeen = centerDetected; vs.stableAtMax++ } if (!vs.boundaryHit && vs.stableAtMax >= 2 && centerDetected < vs.maxCenterSeen) { vs.boundaryHit = true - return PlacementGuideResult("너무 올라갔습니다. 아래로 내리세요 ↓", "arrow.down", 30, + return PlacementGuideResult("Too high. Lower the sensor ↓", "arrow.down", 30, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } if (vs.boundaryHit) { if (centerDetected < vs.maxCenterSeen) { - return PlacementGuideResult("조금 더 아래로 ↓", "arrow.down", 35, + return PlacementGuideResult("Lower a bit more ↓", "arrow.down", 35, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } vs.boundaryHit = false; vs.stableAtMax = 0 val l4 = urineLens.getOrElse(4) { 0 }; val l5 = urineLens.getOrElse(5) { 0 } - val lrHint = when { l4 > 0 && l5 > 0 && l4 > l5 -> "최적 위치! 오른쪽으로 →"; l4 > 0 && l5 > 0 && l5 > l4 -> "최적 위치! 왼쪽으로 ←"; else -> "최적 위치! 좌우 조절하세요 ↔" } + val lrHint = when { l4 > 0 && l5 > 0 && l4 > l5 -> "Optimal! Move right →"; l4 > 0 && l5 > 0 && l5 > l4 -> "Optimal! Move left ←"; else -> "Optimal! Adjust left/right ↔" } val lrIcon = when { l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right"; l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.left"; else -> "arrow.left.arrow.right" } return PlacementGuideResult(lrHint, lrIcon, 40, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL) } - return PlacementGuideResult("센서를 위로 천천히 올리세요 ↑", "arrow.up", centerDetected * 10, + return PlacementGuideResult("Gradually raise the sensor ↑", "arrow.up", centerDetected * 10, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -959,15 +959,15 @@ private fun computePlacementGuide( if (centerDetected < 3) { val hint: String; val icon: String when { - (ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" } - (ch0 || ch1) && !ch3 && !ch2 -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" } - else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" } + (ch3 || ch2) && !ch0 && !ch1 -> { hint = "Gradually lower the sensor ↓"; icon = "arrow.down" } + (ch0 || ch1) && !ch3 && !ch2 -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" } + else -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" } } return PlacementGuideResult(hint, icon, centerDetected * 10, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } if (!ch0 && ch3) { - return PlacementGuideResult("CH0이 약합니다. 센서를 아래로 조금 내리세요 ↓", "arrow.down", 25, + return PlacementGuideResult("CH0 weak. Lower the sensor slightly ↓", "arrow.down", 25, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } @@ -980,9 +980,9 @@ private fun computePlacementGuide( val l4 = urineLens.getOrElse(4) { 0 } val l5 = urineLens.getOrElse(5) { 0 } val lrHint = when { - l4 > 0 && l5 > 0 && l4 > l5 -> "오른쪽으로 이동하세요 →" - l4 > 0 && l5 > 0 && l5 > l4 -> "왼쪽으로 이동하세요 ←" - else -> "좌우로 천천히 이동하세요 ↔" + l4 > 0 && l5 > 0 && l4 > l5 -> "Move to the right →" + l4 > 0 && l5 > 0 && l5 > l4 -> "Move to the left ←" + else -> "Slowly move left or right ↔" } val lrIcon = when { l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right" @@ -990,15 +990,15 @@ private fun computePlacementGuide( else -> "arrow.left.arrow.right" } return PlacementGuideResult( - "상하 위치 OK! $lrHint", + "Vertical OK! $lrHint", lrIcon, 40, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL) } else { return if (len3 > len0 * 2) { - PlacementGuideResult("센서를 아래로 조금 내리세요 ↓", "arrow.down", 25, + PlacementGuideResult("Lower the sensor slightly ↓", "arrow.down", 25, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } else { - PlacementGuideResult("센서를 위로 조금 올리세요 ↑", "arrow.up", 25, + PlacementGuideResult("Raise the sensor slightly ↑", "arrow.up", 25, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL) } } @@ -1012,12 +1012,12 @@ private fun computePlacementGuide( val ch5 = detected.getOrElse(5) { false } if (centerDetected < 3) { - return PlacementGuideResult("상하 위치 벗어남 — 위아래 다시 조절", "arrow.up.arrow.down", 20, + return PlacementGuideResult("Position lost — readjust up/down", "arrow.up.arrow.down", 20, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.VERTICAL) } if (!ch4 && !ch5) { - return PlacementGuideResult("좌우 센서가 감지되지 않습니다. 좌우로 이동하세요 ↔", "arrow.left.arrow.right", 40, + return PlacementGuideResult("Lateral sensors not detected. Move left/right ↔", "arrow.left.arrow.right", 40, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) } @@ -1027,11 +1027,11 @@ private fun computePlacementGuide( if (lrSymmetry >= 0.7) { return PlacementGuideResult( - "좌우 대칭 OK! 최종 확인 중...", + "Symmetry OK! Final check...", "checkmark.circle", 60, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN) } else { - val hint = if (len4 > len5) "센서를 오른쪽으로 이동 →" else "센서를 왼쪽으로 이동 ←" + val hint = if (len4 > len5) "Move sensor right →" else "Move sensor left ←" val icon = if (len4 > len5) "arrow.right" else "arrow.left" return PlacementGuideResult(hint, icon, 45, phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL) @@ -1058,7 +1058,7 @@ private fun computePlacementGuide( } if (depths.size < 3) { - return PlacementGuideResult("위치 벗어남 — 다시 조절", "exclamationmark.triangle", 30, + return PlacementGuideResult("Position lost — readjust", "exclamationmark.triangle", 30, phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL) } @@ -1089,7 +1089,7 @@ private fun computePlacementGuide( if (isPass) { val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70) return PlacementGuideResult( - "Green! 최적 위치 확인 (CV=${"%.2f".format(cv)})", + "Green! Optimal position (CV=${"%.2f".format(cv)})", "checkmark.circle", score, cv, true, lrDev, lrPass, phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN) } else { diff --git a/app/src/main/res/drawable/sensor_device.png b/app/src/main/res/drawable/sensor_device.png new file mode 100644 index 0000000..5648596 Binary files /dev/null and b/app/src/main/res/drawable/sensor_device.png differ