From 5e39237a360da0f5d1b22c8e7806d328b6de8252 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 16 Jun 2026 10:32:55 +0900 Subject: [PATCH] =?UTF-8?q?ux:=20Sensor=20Alignment=204=EC=A2=85=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(demo-final)=20=E2=80=94=20feature/tab-nav?= =?UTF-8?q?igation=20=EC=99=80=20=EB=8F=99=EB=93=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 배너 거짓 양성 — BLE 데이터 수신 중인데 "기기 응답 없음" 뜨던 문제 - BleManager.lastBleRxAt 추가, reb 수신마다 갱신 - FirmwareWarningBanner: 최근 5초 내 RX 있으면 timedOut 무시 - 1초 tick 으로 자동 재평가 2) Sensor Alignment 타이틀 26 → 32sp ExtraBold 3) 본문 레이아웃 재배치 기존: Step → Hint → 일러스트 변경: Step → 일러스트 (위로) → Hint (그림 아래, 22sp) 4) PNG 화살표 더 크게 + 살색 안 비치는 깜빡임 - arrowDrawW 90 → 130 (44% 키움, 두꺼움 효과) - alpha 1.0 ↔ 0.85 (was 1.0 ↔ 0.35) — 살색 비침 차단 - tween 180 → 220ms 추가: - mls 로그 파서 16bit BE 수정 (feature 와 동일 — 이전엔 항상 "mode 0" 표시) Co-Authored-By: Claude Opus 4.7 --- .../medilightv2android/ble/BleManager.kt | 10 ++++- .../ui/components/FirmwareWarningBanner.kt | 16 ++++++- .../ui/views/monitoring/PlacementGuideView.kt | 44 ++++++++++--------- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index ace2b0e..c4a2d7c 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -85,6 +85,9 @@ class BleManager private constructor(private val context: Context) { val firmwareVersion = mutableStateOf("") // "VBTFW0116" val hardwareVersion = mutableStateOf("") // "VBTHW0100" val serialNumber = mutableStateOf("") // "VBT26040001" + /** 마지막으로 의미있는 BLE RX (reb 등) 가 도착한 시각 ms. + * FirmwareWarningBanner false-positive 차단용. */ + val lastBleRxAt = mutableStateOf(0L) /** "VBTFW0116" → 116, 형식 안 맞으면 null. */ fun parseFirmwareBuild(raw: String): Int? { @@ -460,7 +463,11 @@ class BleManager private constructor(private val context: Context) { "mag" -> "NIRS gain query" "mcj" -> "NIRS MCJ query" "mid" -> "device info query" - "mls" -> "LED mode ${if (data.size > 4) data[4].toInt() and 0xFF else "?"}" + // mls 는 buildCommandBE 로 16bit BE 인코딩 → data[4]=High, data[5]=Low + "mls" -> "LED mode ${ + if (data.size > 5) ((data[4].toInt() and 0xFF) shl 8) or (data[5].toInt() and 0xFF) + else "?" + }" "msp" -> "IMU query" "mbb" -> "full measurement (battery+IMU+temp+6ch)" else -> "" @@ -1130,6 +1137,7 @@ class BleManager private constructor(private val context: Context) { val chNum = if (data.size > 5) data[5].toInt() and 0xFF else -1 debugLogger.rx("reb", data.size, "session=$chSession ch=$chNum ${samples}samples") piezoCollector.addPacket(data) + lastBleRxAt.value = System.currentTimeMillis() // 기기 alive 마크 } "red:" -> { debugLogger.rx("red", data.size, "continuation") diff --git a/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt b/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt index d0911d4..3f286db 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/components/FirmwareWarningBanner.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.delay fun FirmwareWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) { val fw by bleManager.firmwareVersion val isConnected by bleManager.isConnected + val lastRxAt by bleManager.lastBleRxAt // 미연결 상태에선 펌웨어 배너 일절 노출 X. dev bypass / demo mode / 이전 세션 // 잔여 firmwareVersion 값으로 인한 false positive 방지. @@ -57,8 +58,21 @@ fun FirmwareWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) } } + // deviceAlive 재계산용 매초 tick + var tick by remember { mutableStateOf(0) } + LaunchedEffect(isConnected) { + while (isConnected) { + delay(1000) + tick++ + } + } + @Suppress("UNUSED_EXPRESSION") tick + // 최근 5초 내 BLE 데이터 수신 있으면 기기 alive — "기기 응답 없음" false positive 차단. + val deviceAlive = lastRxAt > 0 && (System.currentTimeMillis() - lastRxAt) < 5_000 + val outdated = bleManager.isFirmwareOutdated - if (!outdated && !timedOut) return + val showTimedOut = timedOut && !deviceAlive + if (!outdated && !showTimedOut) return val bgColor = if (outdated) Color(0xFFFF9500) else Color(0xFFD32F2F) val title = if (outdated) "펌웨어 업데이트 필요" else "기기 응답 없음" 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 8d9b8c6..20677ff 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 @@ -199,10 +199,10 @@ fun PlacementGuideView(appState: AppState) { } } } - // (B) 타이틀 — Box.Center로 화면 절대 가운데 (← 폭과 무관) + // (B) 타이틀 — Box.Center로 화면 절대 가운데 (← 폭과 무관). 26→32sp 강조. Text( text = "Sensor Alignment", - fontSize = 26.sp, + fontSize = 32.sp, fontWeight = FontWeight.ExtraBold, color = Color.Black ) @@ -322,26 +322,15 @@ fun PlacementGuideView(appState: AppState) { append(rawHint) } } - // 회의 피드백: hint 글자는 검정으로 통일 + 키우기. 박스 배경 색은 의미 유지(노랑=조정 필요/초록=Green). - Text( - text = displayHint, - fontSize = 26.sp, - fontWeight = FontWeight.Bold, - color = Color.Black, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .background(hintBg.copy(alpha = 0.7f), RoundedCornerShape(14.dp)) - .padding(horizontal = 16.dp, vertical = 12.dp) - ) - + // 본문 순서 (2026-06-16 변경): Step → 일러스트 → Hint (그림 아래) + // hint Text 본체는 Body Box 뒤로 이동. 값(displayHint 등) 은 위에서 계산. Spacer(modifier = Modifier.height(2.dp)) - // 화살표 깜빡임 — 매 scan refresh 마다 alpha 토글 → 업데이트 가시화. - // (사용자 요청: "refreshing 될 때마다 깜빡이게 — 덥데이트 되는지 확인") + // 화살표 깜빡임 — alpha 1.0 ↔ 0.85 (최솟값 0.85 → body 일러스트 살색 안 비침) + // 이전 0.35 만큼 떨어지면 skin 톤이 비쳐 보이던 사용자 피드백 반영. val arrowAlpha by animateFloatAsState( - targetValue = if (scanCount % 2 == 0) 1f else 0.35f, - animationSpec = tween(durationMillis = 180), + targetValue = if (scanCount % 2 == 0) 1f else 0.85f, + animationSpec = tween(durationMillis = 220), label = "arrowBlink" ) @@ -427,7 +416,7 @@ fun PlacementGuideView(appState: AppState) { // arrow_transparent.png 는 우향(→) 132×112. 회전으로 4방향 처리. // tint = deep orange 700 (#E64A19) — 일러스트 톤 맞춤 + alpha 로 blink. if (!greenZone) { - val arrowDrawW = 90f + val arrowDrawW = 130f // 90 → 130 (44% 키움, 두꺼움 효과) val arrowDrawH = arrowDrawW * (arrowBitmap.height.toFloat() / arrowBitmap.width.toFloat()) val sensorHalfH = h * 0.08f val gap = 20f @@ -482,7 +471,20 @@ fun PlacementGuideView(appState: AppState) { } } - // (힌트 텍스트는 Step 아래, 몸 위에 배치됨) + Spacer(modifier = Modifier.height(8.dp)) + + // ── 가이드 hint — 일러스트 *아래* 로 이동 (사용자 피드백) ── + Text( + text = displayHint, + fontSize = 22.sp, // 26 → 22 (그림이 메인, hint 보조) + fontWeight = FontWeight.Bold, + color = Color.Black, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .background(hintBg.copy(alpha = 0.7f), RoundedCornerShape(14.dp)) + .padding(horizontal = 16.dp, vertical = 10.dp) + ) Spacer(modifier = Modifier.height(8.dp))