package com.medithings.vesiscan.dev import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.medithings.vesiscan.ble.BleManager import com.medithings.vesiscan.ui.theme.MlCritical import com.medithings.vesiscan.ui.theme.MlSecondaryText import com.medithings.vesiscan.ui.theme.MlSuccess /** * Dev 전용 · 기기 배터리 성능 테스트 패널. * * mim? 1초 / mbb? 10초 고정 부하를 걸고 송수신 카운터 · 배터리 전압 추이를 실시간 표시. * 루프 소유자는 [BatteryDrainTester] (Handler 기반) 이므로 이 다이얼로그를 닫아도 * 테스트는 계속 진행된다. 종료는 "정지" 버튼으로만. */ @Composable fun BatteryDrainTestDialog( bleManager: BleManager, onDismiss: () -> Unit, ) { val running by BatteryDrainTester.isRunning val connected by bleManager.isConnected val battPct by bleManager.batteryLevel val imuSent by BatteryDrainTester.imuSent val imuRecv by BatteryDrainTester.imuRecv val imuSkipped by BatteryDrainTester.imuSkipped val fullSent by BatteryDrainTester.fullSent val fullRecv by BatteryDrainTester.fullRecv val fullSkipped by BatteryDrainTester.fullSkipped val firstMv by BatteryDrainTester.firstBatteryMv val lastMv by BatteryDrainTester.lastBatteryMv val tempC by BatteryDrainTester.lastTempC val lastRxAt by BatteryDrainTester.lastRxAtMs // 경과 시간 1초 tick — 테스트 중일 때만 재구성. var nowMs by remember { mutableStateOf(System.currentTimeMillis()) } LaunchedEffect(running) { while (running) { nowMs = System.currentTimeMillis() kotlinx.coroutines.delay(1000) } nowMs = System.currentTimeMillis() } // nowMs 를 key 로 둬서 tick 마다 다시 계산 (elapsedMs() 자체는 Compose state 가 아님). val elapsed = remember(nowMs, running) { BatteryDrainTester.elapsedMs() } val silenceSec = if (lastRxAt == 0L) -1L else (nowMs - lastRxAt) / 1000 AlertDialog( onDismissRequest = onDismiss, title = { Text("🔋 배터리 성능 테스트 (dev)", fontWeight = FontWeight.Bold, fontSize = 17.sp) }, text = { Column { Text( "mim? 1초 주기 · mbb? 10초 주기 고정 부하.\n" + "다이얼로그를 닫아도 테스트는 계속 진행됩니다.", fontSize = 12.sp, color = MlSecondaryText ) Spacer(Modifier.height(12.dp)) StatRow("상태", if (running) "RUNNING" else "정지", valueColor = if (running) MlSuccess else MlSecondaryText) StatRow("BLE", if (connected) "연결됨" else "끊김", valueColor = if (connected) MlSuccess else MlCritical) StatRow("경과", BatteryDrainTester.formatElapsed(elapsed)) Spacer(Modifier.height(8.dp)) StatRow("mim (IMU)", "$imuRecv / $imuSent" + if (imuSkipped > 0) " (skip $imuSkipped)" else "") StatRow("mbb (전체정보)", "$fullRecv / $fullSent" + if (fullSkipped > 0) " (skip $fullSkipped)" else "") StatRow( "마지막 응답", when { silenceSec < 0 -> "—" else -> "${silenceSec}초 전" }, valueColor = if (running && silenceSec > 15) MlCritical else Color.Unspecified ) Spacer(Modifier.height(8.dp)) StatRow("배터리", if (battPct > 0) "$battPct%" else "—") StatRow( "전압", if (lastMv > 0) { val delta = lastMv - firstMv "${lastMv}mV" + if (firstMv > 0 && delta != 0) " (${if (delta > 0) "+" else ""}$delta)" else "" } else "—" ) StatRow("온도", if (tempC.isNaN()) "—" else "%.2f°C".format(tempC)) if (elapsed > 0 && firstMv > 0 && lastMv in 1 until firstMv) { val mvPerHour = (firstMv - lastMv) * 3_600_000.0 / elapsed Spacer(Modifier.height(8.dp)) StatRow("소모율", "%.1f mV/h".format(mvPerHour)) } } }, confirmButton = { Button( onClick = { BatteryDrainTester.toggle(bleManager) }, enabled = running || connected, colors = ButtonDefaults.buttonColors( containerColor = if (running) MlCritical else MlSuccess ) ) { Text(if (running) "정지" else "시작", color = Color.White, fontWeight = FontWeight.SemiBold) } }, dismissButton = { TextButton(onClick = onDismiss) { Text("닫기") } } ) } @Composable private fun StatRow(label: String, value: String, valueColor: Color = Color.Unspecified) { Row( modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text(label, fontSize = 13.sp, color = MlSecondaryText) Text( value, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace, color = valueColor ) } } /** * HomeView 하단 dev 진입 버튼. 테스트 진행 중이면 라벨에 경과/카운터를 요약 표시해 * 다이얼로그를 열지 않아도 살아있는지 확인 가능. */ @Composable fun BatteryDrainTestButton( bleManager: BleManager, modifier: Modifier = Modifier, ) { val running by BatteryDrainTester.isRunning var showDialog by remember { mutableStateOf(false) } var nowMs by remember { mutableStateOf(System.currentTimeMillis()) } LaunchedEffect(running) { while (running) { nowMs = System.currentTimeMillis() kotlinx.coroutines.delay(1000) } } // nowMs 를 key 로 둬서 tick 마다 라벨의 경과 시간을 다시 계산. val elapsedLabel = remember(nowMs, running) { BatteryDrainTester.formatElapsed(BatteryDrainTester.elapsedMs()) } Button( onClick = { showDialog = true }, modifier = modifier.height(48.dp), shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( containerColor = if (running) MlCritical else Color(0xFF455A64) ) ) { Text( if (running) "🔋 부하테스트 中 · $elapsedLabel" else "🔋 배터리 성능 테스트 (mim 1s / mbb 10s)", color = Color.White, fontWeight = FontWeight.SemiBold, fontSize = 13.sp ) } if (showDialog) { BatteryDrainTestDialog(bleManager = bleManager, onDismiss = { showDialog = false }) } }