feat(dev): 기기 배터리 성능 테스트용 임시 부하 버튼 (mim 1s / mbb 10s)
프로브 배터리 소모율 실측을 위해 홈 화면(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>
This commit is contained in:
@@ -292,6 +292,12 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
var onReconnectionFailed: (() -> Unit)? = null
|
var onReconnectionFailed: (() -> Unit)? = null
|
||||||
var onImuReceived: ((ByteArray) -> Unit)? = null
|
var onImuReceived: ((ByteArray) -> Unit)? = null
|
||||||
var onMbbHeaderReceived: ((ByteArray) -> Unit)? = null
|
var onMbbHeaderReceived: ((ByteArray) -> Unit)? = null
|
||||||
|
/**
|
||||||
|
* 모든 수신 프레임 tap (3-char 태그 + raw). 파싱이 끝난 뒤 CommandQueue 완료 판정 직전 호출.
|
||||||
|
* 계측/진단 전용 (dev 배터리 부하 테스트) — 기능 로직은 이 콜백에 의존하지 않는다.
|
||||||
|
* ⚠ BLE 콜백 스레드에서 호출됨. 무거운 작업 금지.
|
||||||
|
*/
|
||||||
|
var onResponseFrame: ((String, ByteArray) -> Unit)? = null
|
||||||
|
|
||||||
val isBluetoothReady: Boolean get() = bluetoothAdapter?.isEnabled == true
|
val isBluetoothReady: Boolean get() = bluetoothAdapter?.isEnabled == true
|
||||||
|
|
||||||
@@ -1760,6 +1766,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
// True 면 pending 해제 + 큐에 대기 중이던 다음 명령 dispatch.
|
// True 면 pending 해제 + 큐에 대기 중이던 다음 명령 dispatch.
|
||||||
if (data.size >= 4) {
|
if (data.size >= 4) {
|
||||||
val tag3 = prefix.take(3)
|
val tag3 = prefix.take(3)
|
||||||
|
try { onResponseFrame?.invoke(tag3, data) } catch (_: Exception) {}
|
||||||
commandQueue.onResponseTag(tag3)
|
commandQueue.onResponseTag(tag3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package com.medithings.vesiscan.dev
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import com.medithings.vesiscan.ble.BleManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev 전용 · 기기 배터리 성능(소모율) 테스트 드라이버.
|
||||||
|
*
|
||||||
|
* 목적: 프로브를 고정 트래픽 부하에 두고 배터리가 얼마나 버티는지 실측.
|
||||||
|
*
|
||||||
|
* 부하 프로파일:
|
||||||
|
* - `mim?` (IMU FIFO 15 sample) — **1초 주기**
|
||||||
|
* - `mbb?` (전체정보: battery + IMU + temp + 6ch piezo) — **10초 주기**
|
||||||
|
*
|
||||||
|
* 설계 노트
|
||||||
|
* - 명령 송신은 [BleManager.commandQueue] (depth=1 sequential) 를 그대로 탄다.
|
||||||
|
* 즉 mim 과 mbb 가 겹쳐도 GATT 로는 직렬화되므로 firmware queue overrun
|
||||||
|
* (VBTFW0121 freeze) 위험이 없다.
|
||||||
|
* - 그럼에도 `mim` 은 [BleManager.isMtbBusy] 를 앞에서 확인해 skip 한다.
|
||||||
|
* `sendImuFifoQuery()` 가 `imuCollector.reset()` 을 즉시 (큐 대기 전에) 호출하므로
|
||||||
|
* 진행 중인 mtb/mbb 의 rim 을 파괴할 수 있기 때문. (BLE_PROTOCOL_REFERENCE §2.6)
|
||||||
|
* - 미연결 구간에서는 송신만 skip 하고 루프는 유지한다. BleManager 의 auto-reconnect
|
||||||
|
* 가 복구하면 부하가 자동 재개돼야 장시간(수 시간) 테스트가 끊기지 않는다.
|
||||||
|
* - Handler(mainLooper) 기반이라 화면 이동/백그라운드와 무관하게 계속 돈다.
|
||||||
|
* 반드시 [stop] 으로 명시적으로 끝낼 것.
|
||||||
|
*/
|
||||||
|
object BatteryDrainTester {
|
||||||
|
|
||||||
|
const val IMU_INTERVAL_MS = 1_000L
|
||||||
|
const val FULL_INTERVAL_MS = 10_000L
|
||||||
|
|
||||||
|
// ── 관찰 가능한 상태 (Compose UI 가 구독) ──
|
||||||
|
val isRunning = mutableStateOf(false)
|
||||||
|
/** 테스트 시작 시각 (elapsedRealtime 기준 아님 · wall clock). 0 = 미시작. */
|
||||||
|
val startedAtMs = mutableStateOf(0L)
|
||||||
|
|
||||||
|
val imuSent = mutableStateOf(0)
|
||||||
|
val imuRecv = mutableStateOf(0)
|
||||||
|
/** isMtbBusy · 미연결로 송신을 건너뛴 mim tick 수. */
|
||||||
|
val imuSkipped = mutableStateOf(0)
|
||||||
|
|
||||||
|
val fullSent = mutableStateOf(0)
|
||||||
|
val fullRecv = mutableStateOf(0)
|
||||||
|
val fullSkipped = mutableStateOf(0)
|
||||||
|
|
||||||
|
/** 마지막으로 관측한 배터리 전압 (mV) · rbb 헤더 또는 rsn 응답에서 갱신. 0 = 미측정. */
|
||||||
|
val lastBatteryMv = mutableStateOf(0)
|
||||||
|
/** 테스트 시작 후 처음 관측한 배터리 전압 (mV). 소모량 delta 계산 기준. */
|
||||||
|
val firstBatteryMv = mutableStateOf(0)
|
||||||
|
/** 마지막 rbb 온도 (°C). Float.NaN = 미측정. */
|
||||||
|
val lastTempC = mutableStateOf(Float.NaN)
|
||||||
|
/** 마지막 응답 수신 시각 (wall clock). 0 = 없음. */
|
||||||
|
val lastRxAtMs = mutableStateOf(0L)
|
||||||
|
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var ble: BleManager? = null
|
||||||
|
|
||||||
|
// ── 부하 루프 ──
|
||||||
|
|
||||||
|
private val imuTick = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val b = ble
|
||||||
|
if (b == null || !isRunning.value) return
|
||||||
|
when {
|
||||||
|
!b.isConnected.value -> imuSkipped.value++
|
||||||
|
// mtb/mbb 응답 stream 진행 중 — imuCollector.reset() 이 그 rim 을 파괴하므로 skip.
|
||||||
|
b.isMtbBusy -> imuSkipped.value++
|
||||||
|
else -> {
|
||||||
|
b.sendImuFifoQuery()
|
||||||
|
imuSent.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, IMU_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val fullTick = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
val b = ble
|
||||||
|
if (b == null || !isRunning.value) return
|
||||||
|
if (!b.isConnected.value) {
|
||||||
|
fullSkipped.value++
|
||||||
|
} else {
|
||||||
|
b.sendFullMeasurement()
|
||||||
|
fullSent.value++
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, FULL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⚠ BLE 콜백 스레드에서 호출됨. Compose state write 만 (기존 batteryLevel 갱신과 동일 패턴). */
|
||||||
|
private val frameTap: (String, ByteArray) -> Unit = { tag, data ->
|
||||||
|
when (tag) {
|
||||||
|
// 완결된 IMU 프레임 1건 = mim 응답 1건.
|
||||||
|
"rim" -> { imuRecv.value++; lastRxAtMs.value = System.currentTimeMillis() }
|
||||||
|
// 저 MTU 분할 응답 — 마지막 chunk 에서만 1건으로 집계.
|
||||||
|
"ric" -> if (isRicLastChunk(data)) {
|
||||||
|
imuRecv.value++; lastRxAtMs.value = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
// 전체정보 헤더 = mbb 응답 1건.
|
||||||
|
// [tag 4B][batt 2B][IMU ...][temp 2B][crc 2B] = 22B
|
||||||
|
// ⚠ 문서(§10)의 필드 폭 합이 22B 와 맞지 않아 IMU 폭이 불확실하다.
|
||||||
|
// batt 는 첫 필드라 offset 4 로 확정, temp 는 CRC 직전 필드라
|
||||||
|
// `size - 4` 로 잡으면 IMU 폭(2B/12B) 과 무관하게 맞는다.
|
||||||
|
"rbb" -> {
|
||||||
|
fullRecv.value++
|
||||||
|
lastRxAtMs.value = System.currentTimeMillis()
|
||||||
|
if (data.size >= 12) {
|
||||||
|
noteBatteryMv(be16(data, 4))
|
||||||
|
val t = be16(data, data.size - 4) / 100f
|
||||||
|
if (t in 0f..80f) lastTempC.value = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 30초 주기 msn? polling 응답 — 부하 테스트가 굴리진 않지만 전압 소스로 활용.
|
||||||
|
"rsn" -> if (data.size >= 6) noteBatteryMv(be16(data, 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun noteBatteryMv(mv: Int) {
|
||||||
|
if (mv !in 2000..5000) return // 파싱 오류 / 미측정 값 방어
|
||||||
|
if (firstBatteryMv.value == 0) firstBatteryMv.value = mv
|
||||||
|
lastBatteryMv.value = mv
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun be16(data: ByteArray, idx: Int): Int =
|
||||||
|
((data[idx].toInt() and 0xFF) shl 8) or (data[idx + 1].toInt() and 0xFF)
|
||||||
|
|
||||||
|
/** ric: [tag 4B][off 2B][total 2B][chunk 2B][IMU 12B×chunk][crc 2B] */
|
||||||
|
private fun isRicLastChunk(data: ByteArray): Boolean {
|
||||||
|
if (data.size < 10) return false
|
||||||
|
val off = be16(data, 4)
|
||||||
|
val total = be16(data, 6)
|
||||||
|
val chunk = be16(data, 8)
|
||||||
|
return total > 0 && off + chunk >= total
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 제어 ──
|
||||||
|
|
||||||
|
fun start(bleManager: BleManager) {
|
||||||
|
if (isRunning.value) return
|
||||||
|
ble = bleManager
|
||||||
|
resetCounters()
|
||||||
|
startedAtMs.value = System.currentTimeMillis()
|
||||||
|
isRunning.value = true
|
||||||
|
bleManager.onResponseFrame = frameTap
|
||||||
|
bleManager.debugLogger.info(
|
||||||
|
"BATT_DRAIN start · mim=${IMU_INTERVAL_MS}ms mbb=${FULL_INTERVAL_MS}ms"
|
||||||
|
)
|
||||||
|
// mim 을 먼저 태워 첫 tick 이 mbb 응답과 겹치지 않게 한다.
|
||||||
|
handler.post(imuTick)
|
||||||
|
handler.postDelayed(fullTick, FULL_INTERVAL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
if (!isRunning.value) return
|
||||||
|
isRunning.value = false
|
||||||
|
handler.removeCallbacks(imuTick)
|
||||||
|
handler.removeCallbacks(fullTick)
|
||||||
|
ble?.let { b ->
|
||||||
|
if (b.onResponseFrame === frameTap) b.onResponseFrame = null
|
||||||
|
b.debugLogger.info(
|
||||||
|
"BATT_DRAIN stop · elapsed=${elapsedMs() / 1000}s " +
|
||||||
|
"mim=${imuRecv.value}/${imuSent.value} mbb=${fullRecv.value}/${fullSent.value} " +
|
||||||
|
"batt=${firstBatteryMv.value}→${lastBatteryMv.value}mV"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ble = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggle(bleManager: BleManager) {
|
||||||
|
if (isRunning.value) stop() else start(bleManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetCounters() {
|
||||||
|
imuSent.value = 0; imuRecv.value = 0; imuSkipped.value = 0
|
||||||
|
fullSent.value = 0; fullRecv.value = 0; fullSkipped.value = 0
|
||||||
|
lastBatteryMv.value = 0; firstBatteryMv.value = 0
|
||||||
|
lastTempC.value = Float.NaN
|
||||||
|
lastRxAtMs.value = 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
fun elapsedMs(): Long =
|
||||||
|
if (startedAtMs.value == 0L) 0L else System.currentTimeMillis() - startedAtMs.value
|
||||||
|
|
||||||
|
/** "1h 23m 45s" 포맷. */
|
||||||
|
fun formatElapsed(ms: Long): String {
|
||||||
|
val total = ms / 1000
|
||||||
|
val h = total / 3600
|
||||||
|
val m = (total % 3600) / 60
|
||||||
|
val s = total % 60
|
||||||
|
return if (h > 0) "${h}h ${m}m ${s}s" else if (m > 0) "${m}m ${s}s" else "${s}s"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -158,6 +158,18 @@ fun HomeView(appState: AppState) {
|
|||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.home_clinical_mode_dev), color = Color.White, fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.home_clinical_mode_dev), color = Color.White, fontWeight = FontWeight.SemiBold)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2026-08-21 · 기기 배터리 성능(소모율) 실측용 임시 부하 테스트.
|
||||||
|
// mim? 1초 / mbb? 10초 고정 주기. PiezoMonitoring 에서 연결한 뒤 홈으로
|
||||||
|
// 돌아와 실행. 루프는 BatteryDrainTester (Handler) 가 소유하므로 화면을
|
||||||
|
// 벗어나도 계속 돌고, "정지" 를 눌러야 끝난다.
|
||||||
|
com.medithings.vesiscan.dev.BatteryDrainTestButton(
|
||||||
|
bleManager = remember { com.medithings.vesiscan.ble.BleManager.getInstance(context) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 32.dp)
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// 2026-08-03 (cloud-mvp 672e679 이식): 프로브 firmware 업데이트 진입점.
|
// 2026-08-03 (cloud-mvp 672e679 이식): 프로브 firmware 업데이트 진입점.
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|||||||
+5
-1
@@ -889,7 +889,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
// 2026-07-08: isMtbBusy 시 skip. 다른 화면 (Alignment) 이 mtb 폴링 중이면
|
// 2026-07-08: isMtbBusy 시 skip. 다른 화면 (Alignment) 이 mtb 폴링 중이면
|
||||||
// mim 의 imuCollector.reset() 이 mtb 의 rim 파괴 + GATT queue 혼잡 →
|
// mim 의 imuCollector.reset() 이 mtb 의 rim 파괴 + GATT queue 혼잡 →
|
||||||
// firmware freeze 실측 확인. 다음 tick 에 재시도.
|
// firmware freeze 실측 확인. 다음 tick 에 재시도.
|
||||||
if (!bleManager.isMtbBusy) bleManager.sendImuFifoQuery()
|
// 2026-08-21: dev 배터리 부하 테스트가 이미 mim 을 1초 주기로 돌리는 중이면
|
||||||
|
// 여기서 또 쏘지 않는다 (부하 프로파일 오염 + 중복 트래픽 방지).
|
||||||
|
if (!bleManager.isMtbBusy &&
|
||||||
|
!com.medithings.vesiscan.dev.BatteryDrainTester.isRunning.value
|
||||||
|
) bleManager.sendImuFifoQuery()
|
||||||
}
|
}
|
||||||
// isAutoMeasuring=true 면 mtb 의 rim: 가 알아서 posture 갱신, mim skip
|
// isAutoMeasuring=true 면 mtb 의 rim: 가 알아서 posture 갱신, mim skip
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user