From eb5affc6b8c2806c04e7902a1726e5b67f8c6689 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 24 Apr 2026 11:31:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=B8=A1=EC=A0=95=20?= =?UTF-8?q?IMU=201=EC=B4=88=20+=20=EC=A0=84=EC=B2=B4=EC=B8=A1=EC=A0=95(mbb?= =?UTF-8?q?)=2010=EC=B4=88=20=EB=A3=A8=ED=94=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자동측정 변경 (기존: 30초 maa → 신규: 1초 msp + 10초 mbb): - 1~9초: msp(IMU query) → rsp(accel+gyro) 1초 간격 - 10초째: mbb(전체측정) → rbb(헤더) + reb×6 + raa - 기존 maa → mbb(sendFullMeasurement)로 전환 - mbb 응답은 rbb: 헤더 후 reb:/raa: 동일 파이프라인 BLE 명령어 추가: - sendImuQuery(): msp 전송 - sendFullMeasurement(): mbb 전송 (piezoCollector 연동) - sendLedMode(state): mls 전송 응답 파싱 추가: - rxs: → "cmd_not_supported" (mid? 미지원 기기 대응) - rbb: → full measurement header - rsp: → IMU data Co-Authored-By: Claude Opus 4.6 (1M context) --- .../medilightv2android/ble/BleManager.kt | 28 +++++++++++++++++ .../views/monitoring/PiezoMonitoringView.kt | 31 +++++++++++++------ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 74ed71f..762f69a 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -85,6 +85,8 @@ class BleManager private constructor(private val context: Context) { var onPiezoDataReceived: ((ByteArray) -> Unit)? = null var onUnexpectedDisconnect: (() -> Unit)? = null var onReconnectionFailed: (() -> Unit)? = null + var onImuReceived: ((ByteArray) -> Unit)? = null + var onMbbHeaderReceived: ((ByteArray) -> Unit)? = null val isBluetoothReady: Boolean get() = bluetoothAdapter?.isEnabled == true @@ -282,6 +284,8 @@ class BleManager private constructor(private val context: Context) { "mcj" -> "NIRS MCJ query" "mid" -> "device info query" "mls" -> "LED mode ${if (data.size > 4) data[4].toInt() and 0xFF else "?"}" + "msp" -> "IMU query" + "mbb" -> "full measurement (battery+IMU+temp+6ch)" else -> "" } logd { "sendRaw: $cmdPreview (${data.size} bytes)" } @@ -353,6 +357,16 @@ class BleManager private constructor(private val context: Context) { sendRaw(CRC16.buildCommand("mls", intArrayOf(state))) } + fun sendImuQuery() { + sendRaw(CRC16.buildCommand("msp")) + } + + /** 전체 측정: 배터리+IMU+온도+6ch piezo (rbb→reb×6→raa) */ + fun sendFullMeasurement() { + piezoCollector.startMultiChannel(6) + sendRaw(CRC16.buildCommand("mbb")) + } + fun startBatteryPolling() { batteryTimer?.let { handler.removeCallbacks(it) } sendBatteryQuery() @@ -789,6 +803,20 @@ class BleManager private constructor(private val context: Context) { val state = if (data.size >= 5) data[4].toInt() and 0xFF else -1 debugLogger.rx("rls", data.size, "led_state=$state") } + "rxs:" -> { + val echoCmd = if (data.size > 4) String(data, 4, minOf(data.size - 4, 10), Charsets.US_ASCII).trim() else "?" + debugLogger.rx("rxs", data.size, "cmd_not_supported: $echoCmd") + } + "rbb:" -> { + debugLogger.rx("rbb", data.size, "full measurement header (battery+IMU+temp)") + onMbbHeaderReceived?.invoke(data) + } + "rsp:" -> { + if (data.size >= 16) { + debugLogger.rx("rsp", data.size, "IMU data") + } + onImuReceived?.invoke(data) + } else -> { val hex = data.take(8).joinToString(" ") { "%02X".format(it) } debugLogger.rx("???", data.size, "unknown prefix='$prefix' hex=$hex") diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index 51c7656..1ed2348 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -100,8 +100,8 @@ fun PiezoMonitoringView(appState: AppState) { return } - // Real BLE measurement — 6-channel - Log.d("PiezoMonitor", "Sending maa (all channels)...") + // Real BLE measurement — 6-channel (maa or mbb) + Log.d("PiezoMonitor", "Sending measurement command...") bleManager.piezoCollector.onMultiChannelComplete = { channels -> handler.post { @@ -181,7 +181,7 @@ fun PiezoMonitoringView(appState: AppState) { } bleManager.piezoCollector.onLog = { msg -> Log.d("PiezoCollector", msg) } - bleManager.sendAllChannels() + bleManager.sendFullMeasurement() // Timeout with auto-retry (15s) handler.postDelayed({ @@ -190,7 +190,7 @@ fun PiezoMonitoringView(appState: AppState) { measureRetryCount++ Log.w("PiezoMonitor", "Timeout — auto-retry $measureRetryCount/$maxMeasureRetries") bleManager.piezoCollector.reset() - bleManager.sendAllChannels() + bleManager.sendFullMeasurement() // Second timeout for retry handler.postDelayed({ if (!isMeasuring) return@postDelayed @@ -214,21 +214,34 @@ fun PiezoMonitoringView(appState: AppState) { isMeasuring = false } + var autoTickCount by remember { mutableIntStateOf(0) } + fun startAutoMeasure() { isAutoMeasuring = true - measure() + autoTickCount = 0 + measure() // 즉시 첫 전체 측정 } fun stopAutoMeasure() { isAutoMeasuring = false + autoTickCount = 0 } - // Auto-measure: repeat every 30s while active + // Auto-measure: 1초 IMU × 9 + 10초째 전체측정(mbb) 반복 LaunchedEffect(isAutoMeasuring) { if (isAutoMeasuring) { while (isAutoMeasuring) { - kotlinx.coroutines.delay(30_000) - if (isAutoMeasuring && !isMeasuring) measure() + kotlinx.coroutines.delay(1000) + if (!isAutoMeasuring) break + autoTickCount++ + + if (autoTickCount % 10 == 0) { + // 10초째: 전체 측정 (mbb → rbb + reb×6 + raa) + if (!isMeasuring) measure() + } else { + // 1~9초: IMU 쿼리 (msp → rsp) + bleManager.sendImuQuery() + } } } } @@ -736,7 +749,7 @@ fun PiezoMonitoringView(appState: AppState) { } Text( - if (isAutoMeasuring) "Measuring every 30s" else "Long press Spot for auto-measurement", + if (isAutoMeasuring) "Auto: IMU 1s + Measure 10s (#$autoTickCount)" else "Long press for auto (IMU 1s + measure 10s)", fontSize = 11.sp, color = if (isAutoMeasuring) Color.Red.copy(alpha = 0.7f) else MlSecondaryText )