From 6287f21a057a1a2ff32f2c18a79c5d8238725285 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 11 May 2026 10:07:15 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=84=BC=EC=84=9C=20=EB=B9=84=EC=9C=A8?= =?UTF-8?q?=20=EA=B8=B0=EB=B0=98=20+=20LED=20=EA=B9=9C=EB=B9=A1=EC=9E=84?= =?UTF-8?q?=20+=20=EB=94=94=EB=B0=94=EC=9A=B4=EC=8A=A4=20+=20gated=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Placement] - 센서 이미지 비율 기반 (너비 35%, BiasAlignment 0.44) — 태블릿 대응 - LED 깜빡임 수정: greenHeld/failCount<3 동안 LED 5 안 보냄 - 디바운스: phaseProtected 중 lastHintChangeMs 안 건드림 (화살표 안 나오던 버그) - Step 2 전환 힌트 항상 "Checking left/right balance..." - GREEN에서 ch4/ch5 개별 탈락 시 방향 안내 (lrDev NaN 처리) [도넛차트] - Fill 카드: 윗줄 65%, 아랫줄 325/500 mL - ml → mL 통일 - Spot 완료 시 displayMaxVolumeMl 즉시 갱신 - Auto 윈도우 < 5일 때 Current Measurement "—" [기타] - V41 gated 체크 + 최소 len≥3 (ungated 피크 안 보임) - CCCD 실패(status 133) 500ms 후 1회 재시도 - 기기 스캔 사전순 정렬 - 힌트 점 순환 (./.../...) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../medilightv2android/ble/BleManager.kt | 26 ++++++++++- .../ui/views/connection/DeviceScanView.kt | 2 +- .../views/monitoring/PiezoMonitoringView.kt | 4 +- .../ui/views/monitoring/PlacementGuideView.kt | 44 ++++++++++--------- 4 files changed, 51 insertions(+), 25 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 6cf1a1a..cae3652 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -828,8 +828,30 @@ class BleManager private constructor(private val context: Context) { logd { "Service ready! (CCCD written)" } } } else { - loge { "CCCD write failed with status: $status" } - connectionError.value = "Notification setup failed (status $status)" + loge { "CCCD write failed with status: $status — retrying in 500ms" } + // GATT 133 등 일시적 오류 → 500ms 후 재시도 + handler.postDelayed({ + try { + val rx = rxCharacteristic + if (rx != null) { + gatt.setCharacteristicNotification(rx, true) + val cccd = rx.getDescriptor(CCCD_UUID) + if (cccd != null) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(cccd, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + @Suppress("DEPRECATION") + cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + @Suppress("DEPRECATION") + gatt.writeDescriptor(cccd) + } + logd { "CCCD retry sent" } + } + } + } catch (e: Exception) { + connectionError.value = "Notification setup failed (status $status)" + } + }, 500) } } } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt index 12841b1..548dfe0 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/connection/DeviceScanView.kt @@ -46,7 +46,7 @@ fun DeviceScanView(appState: AppState) { val filteredDevices = bleManager.discoveredDevices .filter { it.name.startsWith("2025") || it.name.startsWith("VBT") } - .sortedByDescending { it.rssi } + .sortedBy { it.name } val isConnected by bleManager.isConnected val isServiceReady by bleManager.isServiceReady diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index bced260..e6546b2 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -202,7 +202,9 @@ fun PiezoMonitoringView(appState: AppState) { ) val result = v41.detect(sweepInput) for (ch in result.perChannel) { - if (ch.antIdx != null && ch.postIdx != null && ch.ch < 6) { + val gated = ch.v41Diag?.gated == true + if (gated && ch.antIdx != null && ch.postIdx != null && ch.ch < 6 + && (ch.postIdx - ch.antIdx) >= 3) { allWalls[ch.ch] = Pair(ch.antIdx, ch.postIdx) } } 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 265a2bb..279398f 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 @@ -360,20 +360,24 @@ fun PlacementGuideView(appState: AppState) { } // 센서 기기 이미지 (방광 타원 중앙에 정렬) - // 타원 중심 = (navelY+pubisY)/2 ≈ 높이의 68%. Alignment.Center = 50%. 차이 ≈ 18% - val boxHeightDp = with(androidx.compose.ui.platform.LocalDensity.current) { - (this@Box).let { 110.dp } // fallback + // 센서 — 화면 비율 기반 크기/위치 (태블릿 대응) + // 타원 중심 ≈ 높이 72%. BiasAlignment(0, 0.44) ≈ 72% + // 센서 너비 = 박스 너비의 35% + androidx.compose.foundation.layout.BoxWithConstraints( + modifier = Modifier.matchParentSize() + ) { + val sensorW = maxWidth * 0.35f + val sensorH = sensorW * 0.625f + androidx.compose.foundation.Image( + painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device), + contentDescription = "Sensor", + modifier = Modifier + .size(width = sensorW, height = sensorH) + .align(androidx.compose.ui.BiasAlignment(0f, 0.44f)), + contentScale = androidx.compose.ui.layout.ContentScale.Fit, + alpha = 0.95f + ) } - androidx.compose.foundation.Image( - painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device), - contentDescription = "Sensor", - modifier = Modifier - .size(width = 192.dp, height = 120.dp) - .align(Alignment.Center) - .offset(x = 0.dp, y = 95.dp), - contentScale = androidx.compose.ui.layout.ContentScale.Fit, - alpha = 0.95f - ) } @@ -666,22 +670,20 @@ fun PlacementGuideView(appState: AppState) { // Start 후 5초간 같은 phase 내 힌트는 즉시, phase 전환은 3초 유지 val justStarted = (now - startPressedAtMs) < 5000 val hintUpdate = justStarted && !phaseChanged && !phaseProtected - if (hintUpdate || greenChanged || (phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 3000) || (!phaseChanged && !phaseProtected && !greenHeld && debounceCount >= 2 && elapsed >= 1500)) { + if (!phaseProtected && (hintUpdate || greenChanged || (phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 3000) || (!phaseChanged && !greenHeld && debounceCount >= 2 && elapsed >= 1500))) { directionHint = guideResult.hint directionIcon = guideResult.icon placementScore = guideResult.score if (phaseChanged) { lastPhaseChangeMs = now phaseHoldScans = 3 - phaseHoldHint = guideResult.hint // 전환 힌트 저장 + phaseHoldHint = guideResult.hint } placementPhase = guideResult.nextPhase lastHintChangeMs = now if (!guideResult.cv.isNaN()) lastCv = guideResult.cv - } - - // phase 보호 중이면 전환 힌트 강제 유지 (무조건 덮어쓰기) - if (phaseProtected && phaseHoldHint.isNotEmpty()) { + } else if (phaseProtected && phaseHoldHint.isNotEmpty()) { + // 보호 중 → 전환 힌트 강제 유지, lastHintChangeMs 안 건드림 directionHint = phaseHoldHint directionIcon = "checkmark.circle" } @@ -727,8 +729,8 @@ fun PlacementGuideView(appState: AppState) { rTracker.reset() bleManager.debugLogger.info("PLACEMENT_GREEN_EXIT after 3 consecutive fails") } - } else { - // GREEN 아닌 상태에서 실패 + } else if (!greenHeld) { + // GREEN 아닌 상태 + hold 아닌 상태에서 실패 if (lastLedMode != 5) { bleManager.sendLedMode(5); lastLedMode = 5 } bleManager.debugLogger.info("PLACEMENT_${guideResult.phase.name} score=${guideResult.score} otsu=${"%.0f".format(analyzer.lastOtsuThreshold)} hint=${guideResult.hint}") }