f19a56694b
프로브 배터리 소모율 실측을 위해 홈 화면(dev 모드)에 고정 트래픽 부하를 거는 임시 버튼 추가. - BatteryDrainTester: Handler 기반 루프 소유자. mim? 1초 / mbb? 10초 주기. 화면 이동·백그라운드와 무관하게 계속 돌고 "정지" 로만 종료. mim 은 isMtbBusy 앞에서 skip (imuCollector.reset() 이 mtb/mbb 의 rim 파괴 방지 · BLE_PROTOCOL_REFERENCE §2.6). 미연결 구간은 송신만 skip 해 auto- reconnect 후 부하 자동 재개. - BatteryDrainTestDialog/Button: 송수신 카운터 · 배터리 전압 delta · 온도 · 마지막 응답 경과 · mV/h 소모율 실시간 표시. - BleManager.onResponseFrame: 계측 전용 수신 프레임 tap 추가 (기존 콜백 clobber 없이 rim/ric/rbb/rsn 집계). 기능 로직은 이 콜백에 비의존. - PiezoMonitoringView: 부하 테스트 중이면 자체 mim 폴링 skip (중복 트래픽 방지). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
207 lines
8.0 KiB
Kotlin
207 lines
8.0 KiB
Kotlin
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 })
|
|
}
|
|
}
|