feat: ADC 로그 저장 + 기기정보 mid? + LED 모드 + 무한 3초 reconnect
1. ADC 로그: 측정 완료 시 MeasurementLogService.logMeasurement() 호출 → 6채널 raw ADC + 채널별 ant/post/score 저장 (JSONL) 2. 기기 정보: 연결 직후 mid? 전송 → rid: 응답 파싱 → 로그 기록 (HW Version, Serial Number, FW Version 일괄 조회) 3. LED 모드: Placement Guide 연동 - 스캔 시작 → mls?(5) ALIGN_SEARCHING - PASS → mls?(6) ALIGN_COMPLETE - 스캔 종료 → mls?(0) OFF 4. Reconnect: 3/5/10초 5회 → 3초 간격 무한 재시도 (연결됐다가 끊어진 경우, 사용자가 Home 돌아갈 때까지) 5. BLE 로그: mid/mls TX 파싱, rid:/rls: RX 파싱 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
var lastConnectedAddress: String? = null
|
||||
private set
|
||||
private var reconnectAttempt = 0
|
||||
private val maxReconnectAttempts = 5
|
||||
private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000)
|
||||
private val reconnectIntervalMs: Long = 3000
|
||||
private var reconnectTimer: Runnable? = null
|
||||
private var lastRxTimestamp: Long = 0
|
||||
private var watchdogTimer: Runnable? = null
|
||||
@@ -281,6 +280,8 @@ class BleManager private constructor(private val context: Context) {
|
||||
"mqq" -> "NIRS activate"
|
||||
"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 "?"}"
|
||||
else -> ""
|
||||
}
|
||||
logd { "sendRaw: $cmdPreview (${data.size} bytes)" }
|
||||
@@ -344,6 +345,14 @@ class BleManager private constructor(private val context: Context) {
|
||||
sendRaw(CRC16.buildCommand("msn", intArrayOf(0)))
|
||||
}
|
||||
|
||||
fun sendDeviceInfoQuery() {
|
||||
sendRaw(CRC16.buildCommand("mid"))
|
||||
}
|
||||
|
||||
fun sendLedMode(state: Int) {
|
||||
sendRaw(CRC16.buildCommand("mls", intArrayOf(state)))
|
||||
}
|
||||
|
||||
fun startBatteryPolling() {
|
||||
batteryTimer?.let { handler.removeCallbacks(it) }
|
||||
sendBatteryQuery()
|
||||
@@ -437,23 +446,14 @@ class BleManager private constructor(private val context: Context) {
|
||||
// ── Auto Reconnect ──
|
||||
|
||||
private fun scheduleAutoReconnect() {
|
||||
if (reconnectAttempt >= maxReconnectAttempts) {
|
||||
logw { "Max reconnect attempts ($maxReconnectAttempts) reached" }
|
||||
debugLogger.reconnectFailed()
|
||||
isReconnecting.value = false
|
||||
onReconnectionFailed?.invoke()
|
||||
return
|
||||
}
|
||||
|
||||
val delay = reconnectDelays[reconnectAttempt.coerceAtMost(reconnectDelays.size - 1)]
|
||||
logd { "Auto-reconnect attempt ${reconnectAttempt + 1}/$maxReconnectAttempts in ${delay}ms" }
|
||||
debugLogger.reconnecting(reconnectAttempt + 1, maxReconnectAttempts, delay)
|
||||
logd { "Auto-reconnect attempt ${reconnectAttempt + 1} in ${reconnectIntervalMs}ms" }
|
||||
debugLogger.reconnecting(reconnectAttempt + 1, -1, reconnectIntervalMs)
|
||||
isReconnecting.value = true
|
||||
|
||||
cancelReconnectTimer()
|
||||
val timer = Runnable { attemptAutoReconnect() }
|
||||
reconnectTimer = timer
|
||||
handler.postDelayed(timer, delay)
|
||||
handler.postDelayed(timer, reconnectIntervalMs)
|
||||
}
|
||||
|
||||
private var reconnectScanCallback: ScanCallback? = null
|
||||
@@ -522,13 +522,9 @@ class BleManager private constructor(private val context: Context) {
|
||||
try { scanner.stopScan(callback) } catch (_: Exception) {}
|
||||
reconnectScanCallback = null
|
||||
|
||||
if (!isConnected.value && reconnectAttempt < maxReconnectAttempts) {
|
||||
debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found")
|
||||
if (!isConnected.value) {
|
||||
debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found, retrying...")
|
||||
scheduleAutoReconnect()
|
||||
} else if (!isConnected.value) {
|
||||
debugLogger.reconnectFailed()
|
||||
isReconnecting.value = false
|
||||
onReconnectionFailed?.invoke()
|
||||
}
|
||||
}, 8000)
|
||||
}
|
||||
@@ -699,6 +695,7 @@ class BleManager private constructor(private val context: Context) {
|
||||
isServiceReady.value = true
|
||||
startBatteryPolling()
|
||||
startWatchdog()
|
||||
sendDeviceInfoQuery()
|
||||
logd { "Service ready! (CCCD written)" }
|
||||
}
|
||||
} else {
|
||||
@@ -783,6 +780,15 @@ class BleManager private constructor(private val context: Context) {
|
||||
debugLogger.rx("rsn", data.size, "battery=${millivolts}mV (${pct}%)")
|
||||
}
|
||||
}
|
||||
"rid:" -> {
|
||||
val info = if (data.size > 4) String(data, 4, data.size - 4, Charsets.US_ASCII).trim() else "?"
|
||||
debugLogger.rx("rid", data.size, "device_info=$info")
|
||||
debugLogger.info("DEVICE_INFO $info")
|
||||
}
|
||||
"rls:" -> {
|
||||
val state = if (data.size >= 5) data[4].toInt() and 0xFF else -1
|
||||
debugLogger.rx("rls", data.size, "led_state=$state")
|
||||
}
|
||||
else -> {
|
||||
val hex = data.take(8).joinToString(" ") { "%02X".format(it) }
|
||||
debugLogger.rx("???", data.size, "unknown prefix='$prefix' hex=$hex")
|
||||
|
||||
+14
-1
@@ -130,8 +130,20 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// ADC 로그 저장 (매 측정마다)
|
||||
val logService = com.example.medilightv2android.services.MeasurementLogService.getInstance(context)
|
||||
val channelLogs = analysisResult.channels.map { ch ->
|
||||
com.example.medilightv2android.services.ChannelLogEntry(
|
||||
channel = ch.channel,
|
||||
ant = ch.result?.ant,
|
||||
post = ch.result?.post,
|
||||
urineLen = ch.result?.urineLen,
|
||||
score = ch.result?.score
|
||||
)
|
||||
}
|
||||
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||
|
||||
if (centerWallCount >= 3) {
|
||||
// 6ch BV estimation (center 4ch + lateral lr_ratio)
|
||||
val bvResult = estimateBladderVolume6ch(allWalls)
|
||||
if (bvResult != null) {
|
||||
lastVolumeMl = bvResult.volumeMl
|
||||
@@ -139,6 +151,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Log.d("PiezoMonitor", "BV: ${"%.1f".format(bvResult.volumeMl)}ml (6ch, lr=${"%.2f".format(bvResult.lrRatio)}, center=${centerWallCount}/4)")
|
||||
bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount,
|
||||
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
||||
logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC)
|
||||
}
|
||||
} else if (centerWallCount > 0) {
|
||||
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
|
||||
|
||||
+10
-1
@@ -338,14 +338,23 @@ fun PlacementGuideView(appState: AppState) {
|
||||
bleManager.sendAllChannels()
|
||||
}
|
||||
|
||||
// Continuous scan loop (3초 간격)
|
||||
// Continuous scan loop (3초 간격) + LED 모드
|
||||
LaunchedEffect(isContinuousMode) {
|
||||
if (isContinuousMode) {
|
||||
bleManager.sendLedMode(5) // ALIGN_SEARCHING
|
||||
performScan()
|
||||
while (isContinuousMode) {
|
||||
kotlinx.coroutines.delay(3000)
|
||||
if (isContinuousMode && !isScanning) performScan()
|
||||
}
|
||||
bleManager.sendLedMode(0) // OFF
|
||||
}
|
||||
}
|
||||
|
||||
// PASS 시 LED 완료 표시
|
||||
LaunchedEffect(isLocked) {
|
||||
if (isLocked) {
|
||||
bleManager.sendLedMode(6) // ALIGN_COMPLETE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user