feat(debug): Raw BLE capture 토글 버튼 + 로거 추가

용도: IMU FSR / 채널 누락 / packet 구조 진단을 위해 BLE notification 의 raw
      bytes 를 가공 없이 그대로 파일로 저장.

신규 RawBleLogger.kt:
  - startCapture() / stopCapture() / isCapturing
  - 파일: Downloads/VesiScan_RawBLE/yyyy-MM-dd_HHmmss_raw.log
  - 포맷: "HH:mm:ss.SSS dir=RX prefix=rim len=188 hex=72 69 6D 3A ..."
  - 매 notification 마다 한 줄
  - 파일 시작/끝에 헤더/푸터 (start, stop time, packet count)

BleManager.processReceivedData:
  - 진입 직후 RawBleLogger.write(data) 호출
  - capturing 아니면 즉시 return (no overhead)

PiezoMonitoringView 상단 status bar:
  - posture 아이콘 옆에 토글 IconButton
  - 활성: 빨간 RadioButtonChecked, 비활성: 회색 FiberManualRecord
  - 토글 시 Toast 로 파일 경로 / packet count 안내

사용:
  1) PiezoMonitoring 진입 → 빨간 ● 버튼 탭 → "Raw BLE capture started"
  2) BLE 측정 진행 (alignment / live / single scan)
  3) ● 다시 탭 → "N packets saved to YYYY-MM-DD_HHmmss_raw.log"
  4) adb pull /sdcard/Download/VesiScan_RawBLE/ 로 파일 회수
  5) 텍스트 에디터로 hex 분석

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 15:44:32 +09:00
parent eee065a76a
commit 6a06452d7e
3 changed files with 128 additions and 0 deletions
@@ -1104,6 +1104,8 @@ class BleManager private constructor(private val context: Context) {
private fun processReceivedData(data: ByteArray) { private fun processReceivedData(data: ByteArray) {
lastRxTimestamp = System.currentTimeMillis() lastRxTimestamp = System.currentTimeMillis()
// Raw BLE capture (디버그 — 사용자가 토글로 enable 했을 때만 writes)
com.example.medilightv2android.services.RawBleLogger.write(data)
val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII) val prefix = String(data, 0, minOf(4, data.size), Charsets.US_ASCII)
logd { "Received: $prefix (${data.size} bytes)" } logd { "Received: $prefix (${data.size} bytes)" }
when (prefix) { when (prefix) {
@@ -0,0 +1,94 @@
/*
* RawBleLogger — 모든 BLE notification 의 raw bytes 를 시간순으로 파일로 dump.
*
* 용도: IMU FSR / packet 구조 / loss 진단. JSON 가공 거치기 전 원본 그대로 저장.
*
* 파일: Downloads/VesiScan_RawBLE/yyyy-MM-dd_HHmmss_raw.log
* 포맷 (한 줄당 한 notification):
* HH:mm:ss.SSS dir=RX prefix=rim len=188 hex=72 69 6D 3A 00 0F ...
*
* 사용: startCapture() / stopCapture() 토글. 활성화 시 BleManager.processReceivedData
* 가 매 notification 마다 write() 호출.
*/
package com.example.medilightv2android.services
import android.os.Environment
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean
object RawBleLogger {
private val sessionFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
private val timeFmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
private val capturing = AtomicBoolean(false)
@Volatile private var sessionFile: File? = null
@Volatile private var packetCount: Int = 0
val isCapturing: Boolean get() = capturing.get()
val currentFile: File? get() = sessionFile
val count: Int get() = packetCount
private fun getDir(): File {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VesiScan_RawBLE"
)
if (!dir.exists()) dir.mkdirs()
return dir
}
/** 새 파일 만들고 capture 시작. 이미 capturing 중이면 stop 후 새로 시작. */
fun startCapture(): File {
if (capturing.get()) stopCapture()
val f = File(getDir(), "${sessionFmt.format(Date())}_raw.log")
try {
FileWriter(f, false).buffered().use { w ->
w.write("# RawBleLogger session — started ${timeFmt.format(Date())}")
w.newLine()
w.write("# Format: HH:mm:ss.SSS dir=RX prefix=XXX len=N hex=AA BB ...")
w.newLine()
}
} catch (_: Exception) {}
sessionFile = f
packetCount = 0
capturing.set(true)
return f
}
fun stopCapture(): File? {
if (!capturing.get()) return sessionFile
val f = sessionFile
try {
if (f != null) {
FileWriter(f, true).buffered().use { w ->
w.write("# stopped ${timeFmt.format(Date())} — $packetCount packets")
w.newLine()
}
}
} catch (_: Exception) {}
capturing.set(false)
return f
}
/** BleManager 가 매 notification 받을 때마다 호출. capturing 아니면 즉시 return. */
fun write(data: ByteArray) {
if (!capturing.get()) return
val f = sessionFile ?: return
try {
val ts = timeFmt.format(Date())
val prefix = if (data.size >= 4)
String(data, 0, 4, Charsets.US_ASCII).replace(Regex("[^\\x20-\\x7E]"), "?")
else "?"
val hex = data.joinToString(" ") { "%02X".format(it) }
FileWriter(f, true).buffered().use { w ->
w.write("$ts dir=RX prefix=$prefix len=${data.size} hex=$hex")
w.newLine()
}
packetCount++
} catch (_: Exception) {}
}
}
@@ -979,6 +979,38 @@ fun PiezoMonitoringView(appState: AppState) {
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
// Raw BLE capture toggle — IMU/packet 디버그용 (Downloads/VesiScan_RawBLE/)
var rawBleCapturing by remember {
mutableStateOf(com.example.medilightv2android.services.RawBleLogger.isCapturing)
}
IconButton(
onClick = {
if (rawBleCapturing) {
val f = com.example.medilightv2android.services.RawBleLogger.stopCapture()
val n = com.example.medilightv2android.services.RawBleLogger.count
rawBleCapturing = false
android.widget.Toast.makeText(context,
"Raw BLE: $n packets saved to ${f?.name}",
android.widget.Toast.LENGTH_LONG).show()
} else {
val f = com.example.medilightv2android.services.RawBleLogger.startCapture()
rawBleCapturing = true
android.widget.Toast.makeText(context,
"Raw BLE capture started → ${f.name}",
android.widget.Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.size(28.dp)
) {
Icon(
if (rawBleCapturing) Icons.Default.RadioButtonChecked else Icons.Default.FiberManualRecord,
contentDescription = if (rawBleCapturing) "Stop raw BLE capture" else "Start raw BLE capture",
tint = if (rawBleCapturing) Color.Red else Color.Gray.copy(alpha = 0.5f),
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
// Catheter count (탭하면 추가 팝업) // Catheter count (탭하면 추가 팝업)
var showAddCatheter by remember { mutableStateOf(false) } var showAddCatheter by remember { mutableStateOf(false) }
val cathCount = appState.catheterCount.intValue val cathCount = appState.catheterCount.intValue