1df1a4e6cd
배경 (LOG_STORAGE_ANALYSIS 2026-08-04):
§A · ble.log 의 scan= 값이 실제 adc.csv scan_id 대비 1씩 밀림 (off-by-one)
- 원인: measurementResult() 가 AdcCsvLogger.lastScanId 를 읽은 후에
AdcCsvLogger.log() 가 scanCounter 를 증가시켜 실제 CSV 는 N+1 로 기록.
§B · events.jsonl 에 scan_id 필드 부재 → 4개 로그 (adc.csv · imu.csv · ble.log ·
events.jsonl) 간 조인 키 없음. timestamp 밀리초만으로는 autoScanIntervalMs=600ms
에서 신뢰 불가.
§F · BV 실패 케이스가 events.jsonl 에 아무 흔적 없음.
수정:
AdcCsvLogger:
- nextScanId(): Int 신규 · thread-safe counter 증가 후 id 반환
- log(...): 새 scanId 파라미터 · 호출자가 미리 발급한 id 우선 사용
- counterLock 으로 동시성 방어
MeasurementLogService.logMeasurement(...):
- scanId: Int? 파라미터 추가 · events.jsonl 에 "scan_id" 필드 기록
BleDebugLogger.measurementResult(...):
- scanId: Int? 파라미터 · 호출자가 발급한 id 우선 (미제공 시 legacy fallback)
ImuCsvLogger.log(...):
- scanId: Int? 파라미터 · 호출자가 발급한 id 우선
PiezoMonitoringView (5 sites · onMultiChannelComplete):
- 사이클 진입 시 val scanId = AdcCsvLogger.nextScanId() 미리 발급
- useV41Bv · useMethodDBv · effectiveCenterWalls>=4 · totalWallCount>0 ·
no-valid-channels 5경로 모두 scanId 전달
- §F: BV_SKIP · BV_FAIL 케이스도 events.jsonl 에 기록 (null volume)
ClinicalLiveView (1 site):
- autoCapture 시 val sid = AdcCsvLogger.nextScanId() → AdcCsvLogger + ImuCsvLogger
에 동일 id 전달
효과:
ble.log · adc.csv · imu.csv · events.jsonl 이 scan_id 로 정합 조인 가능.
Guardian 앱이 이벤트 재구성 시 timestamp 대신 scan_id 단일 키 사용.
BV 실패도 audit trail 에 기록.
검증: BUILD SUCCESSFUL 8s
68 lines
2.7 KiB
Kotlin
68 lines
2.7 KiB
Kotlin
package com.medithings.vesiscan.services
|
|
|
|
import com.medithings.vesiscan.ble.ImuSample
|
|
import java.io.File
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
|
|
/**
|
|
* IMU 샘플을 CSV로 저장. 임상 R&D 모드 한정.
|
|
*
|
|
* 파일 위치 (임상 모드일 때):
|
|
* Downloads/VesiScan_Sessions/{folder}/imu.csv
|
|
*
|
|
* 한 번의 mtb? 응답에서 15 샘플 → CSV 15행. scan_id로 같은 측정의 piezo adc.csv와 매칭 가능.
|
|
*
|
|
* Sensitivity:
|
|
* ax/ay/az: g (Accel ±4g, 1 LSB = 1/8192 g)
|
|
* gx/gy/gz: dps (Gyro ±500 dps, 1 LSB = 1/65.536 dps)
|
|
*
|
|
* Sample 순서: 한 응답 안에서 oldest→newest. sample_idx 컬럼으로 명시.
|
|
*/
|
|
object ImuCsvLogger {
|
|
|
|
private val timeFmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
|
|
|
|
/**
|
|
* 임상 모드 한정. ClinicalSessionStore.currentLogDir()가 있을 때만 저장.
|
|
* samples를 oldest→newest 순서로 그대로 기록.
|
|
*/
|
|
fun log(samples: List<ImuSample>, scanId: Int? = null) {
|
|
val dir = ClinicalSessionStore.currentLogDir() ?: return
|
|
if (samples.isEmpty()) return
|
|
try {
|
|
dir.mkdirs()
|
|
val file = File(dir, "imu.csv")
|
|
val isNew = !file.exists() || file.length() == 0L
|
|
// 2026-08-04 §A fix: 호출자가 미리 발급한 scanId 우선. 미제공 시 legacy.
|
|
val sid = scanId ?: AdcCsvLogger.lastScanId
|
|
val timestamp = timeFmt.format(Date())
|
|
val clinicalLabel = ClinicalSessionStore.currentLabel()
|
|
val fwVersion = com.medithings.vesiscan.ble.BleManager.current?.firmwareVersion?.value ?: ""
|
|
|
|
file.bufferedWriter().use { _ -> } // ensure exists
|
|
java.io.FileWriter(file, !isNew).buffered().use { writer ->
|
|
if (isNew) {
|
|
writer.write("scan_id,timestamp,clinical_label,firmware_version,sample_idx,ax_g,ay_g,az_g,gx_dps,gy_dps,gz_dps")
|
|
writer.newLine()
|
|
}
|
|
for ((idx, s) in samples.withIndex()) {
|
|
writer.write(buildString {
|
|
append("$sid,$timestamp,$clinicalLabel,$fwVersion,$idx,")
|
|
append("%.4f".format(s.ax)); append(",")
|
|
append("%.4f".format(s.ay)); append(",")
|
|
append("%.4f".format(s.az)); append(",")
|
|
append("%.4f".format(s.gx)); append(",")
|
|
append("%.4f".format(s.gy)); append(",")
|
|
append("%.4f".format(s.gz))
|
|
})
|
|
writer.newLine()
|
|
}
|
|
}
|
|
} catch (_: Exception) {
|
|
// 로그는 측정 자체를 막으면 안 됨 — silent fail
|
|
}
|
|
}
|
|
}
|