diff --git a/app/src/main/java/com/example/medilightv2android/services/AdcCsvLogger.kt b/app/src/main/java/com/example/medilightv2android/services/AdcCsvLogger.kt new file mode 100644 index 0000000..2ef931d --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/services/AdcCsvLogger.kt @@ -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>) + * @param volumeMl BV 추정 결과 (null이면 측정 실패) + * @param lrRatio lr_ratio 값 + */ + fun log( + channels: List>, + 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 방지 + } + } +} 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 da8924b..cae2124 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 @@ -154,10 +154,12 @@ fun PiezoMonitoringView(appState: AppState) { bleManager.debugLogger.measurementResult(bvResult.volumeMl, bvResult.lrRatio, totalWallCount, (0..5).map { allWalls[it]?.let { w -> w.second - w.first } ?: 0 }) logService.logMeasurement(bvResult.volumeMl, channelLogs, rawADC) + com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, bvResult.volumeMl, bvResult.lrRatio) } } else if (centerWallCount > 0) { Log.w("PiezoMonitor", "Insufficient center channels: $centerWallCount/4 (need 3+)") bleManager.debugLogger.warn("BV_SKIP center=$centerWallCount/4 total=$totalWallCount/6") + com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null) android.widget.Toast.makeText(context, "측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요", android.widget.Toast.LENGTH_SHORT).show() @@ -165,6 +167,8 @@ fun PiezoMonitoringView(appState: AppState) { } else { Log.d("PiezoMonitor", "No valid channels — volume unchanged") 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.LENGTH_SHORT).show()