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:
@@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user