feat: ADC 데이터 일별 CSV 저장 (Downloads/VesiScan_ADC/)

AdcCsvLogger: 측정마다 6채널 ADC를 CSV에 append
- 파일: Downloads/VesiScan_ADC/YYYY-MM-DD.csv
- 행: scan_id × 6채널 (1회 측정 = 6행)
- 열: scan_id, timestamp, volume_ml, lr_ratio, channel, s0~s99
- BV 성공/실패 모두 ADC 저장 (volume_ml 빈칸이면 실패)
- Python: pd.read_csv() → df[df.scan_id==1] 으로 1회 측정 추출

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 10:47:33 +09:00
parent abc70623fa
commit f563b20659
2 changed files with 88 additions and 0 deletions
@@ -0,0 +1,84 @@
package com.example.medilightv2android.services
import android.os.Environment
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* ADC 데이터를 일별 CSV 파일로 저장.
*
* 파일 위치: Downloads/VesiScan_ADC/YYYY-MM-DD.csv
* 행: scan_id × 6채널 (측정 1회 = 6행)
* 열: scan_id, timestamp, volume_ml, lr_ratio, channel, sample_0~sample_99
*/
object AdcCsvLogger {
private val dayFmt = SimpleDateFormat("yyyy-MM-dd", Locale.US)
private val timeFmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
private var scanCounter = 0
private fun getDir(): File {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VesiScan_ADC"
)
if (!dir.exists()) dir.mkdirs()
return dir
}
private fun getTodayFile(): File {
return File(getDir(), "${dayFmt.format(Date())}.csv")
}
/**
* 6채널 ADC 데이터를 CSV에 append.
*
* @param channels 6채널 ADC 버퍼 (List<List<UShort>>)
* @param volumeMl BV 추정 결과 (null이면 측정 실패)
* @param lrRatio lr_ratio 값
*/
fun log(
channels: List<List<UShort>>,
volumeMl: Double? = null,
lrRatio: Double = 1.0
) {
try {
val file = getTodayFile()
val isNew = !file.exists() || file.length() == 0L
scanCounter++
val timestamp = timeFmt.format(Date())
val volStr = volumeMl?.let { "%.1f".format(it) } ?: ""
val lrStr = "%.2f".format(lrRatio)
java.io.FileWriter(file, !isNew).buffered().use { writer ->
if (isNew) {
val header = buildString {
append("scan_id,timestamp,volume_ml,lr_ratio,channel")
for (i in 0 until 100) append(",s$i")
}
writer.write(header)
writer.newLine()
}
for ((chIdx, buffer) in channels.withIndex()) {
val row = buildString {
append("$scanCounter,$timestamp,$volStr,$lrStr,CH$chIdx")
for (sample in buffer) {
append(",${sample.toInt()}")
}
// 100개 미만이면 빈 칸으로 채움
for (i in buffer.size until 100) {
append(",")
}
}
writer.write(row)
writer.newLine()
}
}
} catch (_: Exception) {
// 로깅 실패는 앱 crash 방지
}
}
}
@@ -154,10 +154,12 @@ fun PiezoMonitoringView(appState: AppState) {
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) logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC)
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, bvResult.volumeMl, bvResult.lrRatio)
} }
} 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+)")
bleManager.debugLogger.warn("BV_SKIP center=$centerWallCount/4 total=$totalWallCount/6") bleManager.debugLogger.warn("BV_SKIP center=$centerWallCount/4 total=$totalWallCount/6")
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
android.widget.Toast.makeText(context, android.widget.Toast.makeText(context,
"측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요", "측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요",
android.widget.Toast.LENGTH_SHORT).show() android.widget.Toast.LENGTH_SHORT).show()
@@ -165,6 +167,8 @@ fun PiezoMonitoringView(appState: AppState) {
} else { } else {
Log.d("PiezoMonitor", "No valid channels — volume unchanged") Log.d("PiezoMonitor", "No valid channels — volume unchanged")
bleManager.debugLogger.warn("BV_FAIL no valid channels") bleManager.debugLogger.warn("BV_FAIL no valid channels")
val failADC = channels.sortedBy { it.channel }.map { it.buffer }
com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null)
android.widget.Toast.makeText(context, android.widget.Toast.makeText(context,
"방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.", "방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.",
android.widget.Toast.LENGTH_SHORT).show() android.widget.Toast.LENGTH_SHORT).show()