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
|
var lastConnectedAddress: String? = null
|
||||||
private set
|
private set
|
||||||
private var reconnectAttempt = 0
|
private var reconnectAttempt = 0
|
||||||
private val maxReconnectAttempts = 5
|
private val reconnectIntervalMs: Long = 3000
|
||||||
private val reconnectDelays = longArrayOf(3000, 5000, 10000, 10000, 10000)
|
|
||||||
private var reconnectTimer: Runnable? = null
|
private var reconnectTimer: Runnable? = null
|
||||||
private var lastRxTimestamp: Long = 0
|
private var lastRxTimestamp: Long = 0
|
||||||
private var watchdogTimer: Runnable? = null
|
private var watchdogTimer: Runnable? = null
|
||||||
@@ -281,6 +280,8 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
"mqq" -> "NIRS activate"
|
"mqq" -> "NIRS activate"
|
||||||
"mag" -> "NIRS gain query"
|
"mag" -> "NIRS gain query"
|
||||||
"mcj" -> "NIRS MCJ query"
|
"mcj" -> "NIRS MCJ query"
|
||||||
|
"mid" -> "device info query"
|
||||||
|
"mls" -> "LED mode ${if (data.size > 4) data[4].toInt() and 0xFF else "?"}"
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
logd { "sendRaw: $cmdPreview (${data.size} bytes)" }
|
logd { "sendRaw: $cmdPreview (${data.size} bytes)" }
|
||||||
@@ -344,6 +345,14 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
sendRaw(CRC16.buildCommand("msn", intArrayOf(0)))
|
sendRaw(CRC16.buildCommand("msn", intArrayOf(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendDeviceInfoQuery() {
|
||||||
|
sendRaw(CRC16.buildCommand("mid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendLedMode(state: Int) {
|
||||||
|
sendRaw(CRC16.buildCommand("mls", intArrayOf(state)))
|
||||||
|
}
|
||||||
|
|
||||||
fun startBatteryPolling() {
|
fun startBatteryPolling() {
|
||||||
batteryTimer?.let { handler.removeCallbacks(it) }
|
batteryTimer?.let { handler.removeCallbacks(it) }
|
||||||
sendBatteryQuery()
|
sendBatteryQuery()
|
||||||
@@ -437,23 +446,14 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
// ── Auto Reconnect ──
|
// ── Auto Reconnect ──
|
||||||
|
|
||||||
private fun scheduleAutoReconnect() {
|
private fun scheduleAutoReconnect() {
|
||||||
if (reconnectAttempt >= maxReconnectAttempts) {
|
logd { "Auto-reconnect attempt ${reconnectAttempt + 1} in ${reconnectIntervalMs}ms" }
|
||||||
logw { "Max reconnect attempts ($maxReconnectAttempts) reached" }
|
debugLogger.reconnecting(reconnectAttempt + 1, -1, reconnectIntervalMs)
|
||||||
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)
|
|
||||||
isReconnecting.value = true
|
isReconnecting.value = true
|
||||||
|
|
||||||
cancelReconnectTimer()
|
cancelReconnectTimer()
|
||||||
val timer = Runnable { attemptAutoReconnect() }
|
val timer = Runnable { attemptAutoReconnect() }
|
||||||
reconnectTimer = timer
|
reconnectTimer = timer
|
||||||
handler.postDelayed(timer, delay)
|
handler.postDelayed(timer, reconnectIntervalMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var reconnectScanCallback: ScanCallback? = null
|
private var reconnectScanCallback: ScanCallback? = null
|
||||||
@@ -522,13 +522,9 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
try { scanner.stopScan(callback) } catch (_: Exception) {}
|
try { scanner.stopScan(callback) } catch (_: Exception) {}
|
||||||
reconnectScanCallback = null
|
reconnectScanCallback = null
|
||||||
|
|
||||||
if (!isConnected.value && reconnectAttempt < maxReconnectAttempts) {
|
if (!isConnected.value) {
|
||||||
debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found")
|
debugLogger.warn("RECONNECT_SCAN_TIMEOUT #$reconnectAttempt — device not found, retrying...")
|
||||||
scheduleAutoReconnect()
|
scheduleAutoReconnect()
|
||||||
} else if (!isConnected.value) {
|
|
||||||
debugLogger.reconnectFailed()
|
|
||||||
isReconnecting.value = false
|
|
||||||
onReconnectionFailed?.invoke()
|
|
||||||
}
|
}
|
||||||
}, 8000)
|
}, 8000)
|
||||||
}
|
}
|
||||||
@@ -699,6 +695,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
isServiceReady.value = true
|
isServiceReady.value = true
|
||||||
startBatteryPolling()
|
startBatteryPolling()
|
||||||
startWatchdog()
|
startWatchdog()
|
||||||
|
sendDeviceInfoQuery()
|
||||||
logd { "Service ready! (CCCD written)" }
|
logd { "Service ready! (CCCD written)" }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -783,6 +780,15 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
debugLogger.rx("rsn", data.size, "battery=${millivolts}mV (${pct}%)")
|
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 -> {
|
else -> {
|
||||||
val hex = data.take(8).joinToString(" ") { "%02X".format(it) }
|
val hex = data.take(8).joinToString(" ") { "%02X".format(it) }
|
||||||
debugLogger.rx("???", data.size, "unknown prefix='$prefix' hex=$hex")
|
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) {
|
if (centerWallCount >= 3) {
|
||||||
// 6ch BV estimation (center 4ch + lateral lr_ratio)
|
|
||||||
val bvResult = estimateBladderVolume6ch(allWalls)
|
val bvResult = estimateBladderVolume6ch(allWalls)
|
||||||
if (bvResult != null) {
|
if (bvResult != null) {
|
||||||
lastVolumeMl = bvResult.volumeMl
|
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)")
|
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,
|
bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount,
|
||||||
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
(0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 })
|
||||||
|
logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC)
|
||||||
}
|
}
|
||||||
} else if (centerWallCount > 0) {
|
} else if (centerWallCount > 0) {
|
||||||
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
|
Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)")
|
||||||
|
|||||||
+10
-1
@@ -338,14 +338,23 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
bleManager.sendAllChannels()
|
bleManager.sendAllChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continuous scan loop (3초 간격)
|
// Continuous scan loop (3초 간격) + LED 모드
|
||||||
LaunchedEffect(isContinuousMode) {
|
LaunchedEffect(isContinuousMode) {
|
||||||
if (isContinuousMode) {
|
if (isContinuousMode) {
|
||||||
|
bleManager.sendLedMode(5) // ALIGN_SEARCHING
|
||||||
performScan()
|
performScan()
|
||||||
while (isContinuousMode) {
|
while (isContinuousMode) {
|
||||||
kotlinx.coroutines.delay(3000)
|
kotlinx.coroutines.delay(3000)
|
||||||
if (isContinuousMode && !isScanning) performScan()
|
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