Files
VesiscanClinicalAndroid/app/src/main/java/com/medithings/vesiscan/services/AdcCsvLogger.kt
T

142 lines
5.3 KiB
Kotlin

package com.medithings.vesiscan.services
import android.os.Environment
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* ADC 데이터를 세션별 CSV 파일로 저장.
*
* 파일 위치: Downloads/VesiScan_ADC/YYYY-MM-DD_HHmmss.csv
* newSession() 호출 시 새 파일로 전환 (BLE 연결 시 자동, 수동 호출 가능)
*/
object AdcCsvLogger {
private val sessionFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
private val timeFmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
var lastScanId: Int = 0
private set
private var scanCounter = 0
private var sessionFile: File? = null
private val counterLock = Any()
/**
* 다음 scan_id 를 미리 발급 · scanCounter 증가.
* 2026-08-04: LOG_STORAGE_ANALYSIS §A off-by-one 수정 · 호출자가 미리 id 를
* 확보한 뒤 measurementResult() · logMeasurement() · log() 셋 다 동일 id 로
* 기록해 4개 로그 파일이 정합하게 조인됨.
*/
fun nextScanId(): Int {
synchronized(counterLock) {
scanCounter++
lastScanId = scanCounter
return scanCounter
}
}
private fun getDir(): File {
val dir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"VesiScan_ADC"
)
if (!dir.exists()) dir.mkdirs()
return dir
}
fun newSession() {
scanCounter = 0
lastScanId = 0
sessionFile = File(getDir(), "${sessionFmt.format(Date())}.csv")
}
/**
* 임상 모드: ClinicalSessionStore에 활성 세션이 있으면 그 step 폴더의 adc.csv 사용.
* 일반 모드: 기존 세션 파일 사용.
*/
private fun getFile(): File {
val clinicalDir = ClinicalSessionStore.currentLogDir()
if (clinicalDir != null) {
clinicalDir.mkdirs()
return File(clinicalDir, "adc.csv")
}
if (sessionFile == null) newSession()
return sessionFile!!
}
/**
* ADC CSV 기록.
* @param scanId 호출자가 nextScanId() 로 미리 발급한 id. null 이면 auto-increment (legacy).
*/
fun log(
channels: List<List<UShort>>,
volumeMl: Double? = null,
lrRatio: Double = 1.0,
scanId: Int? = null,
) {
try {
val file = getFile()
val isNew = !file.exists() || file.length() == 0L
val id = scanId ?: nextScanId()
// legacy: scanCounter 는 nextScanId 안에서만 증가 · id 미제공 시에도 동일 경로.
scanCounter = id
lastScanId = id
val timestamp = timeFmt.format(Date())
val volStr = volumeMl?.let { "%.1f".format(it) } ?: ""
val lrStr = "%.2f".format(lrRatio)
val analyzer = com.medithings.vesiscan.managers.PiezoEchoAnalyzer.shared
val thrStr = if (analyzer.useAdaptiveThreshold && analyzer.lastOtsuThreshold > 0)
"%.0f".format(analyzer.lastOtsuThreshold) else "%.0f".format(com.medithings.vesiscan.managers.GreenZoneConstants.lowEchoAmp)
val dpsStr = "%.3f".format(com.medithings.vesiscan.managers.PiezoHW.distancePerSample)
val sgStr = if (analyzer.useSgFilter) "ON" else "OFF"
val clinicalLabel = ClinicalSessionStore.currentLabel() // 일반 모드면 ""
// BLE 펌웨어 버전 (연결 안 됐으면 "")
val fwVersion = com.medithings.vesiscan.ble.BleManager.current?.firmwareVersion?.value ?: ""
java.io.FileWriter(file, !isNew).buffered().use { writer ->
if (isNew) {
val header = buildString {
append("scan_id,timestamp,clinical_label,firmware_version,volume_ml,lr_ratio,threshold,dps,sg_filter,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("$id,$timestamp,$clinicalLabel,$fwVersion,$volStr,$lrStr,$thrStr,$dpsStr,$sgStr,CH$chIdx")
for (sample in buffer) {
append(",${sample.toInt()}")
}
for (i in buffer.size until 100) {
append(",")
}
}
writer.write(row)
writer.newLine()
}
}
} catch (_: Exception) {}
}
/**
* VesiScan_ADC/ 폴더의 keepDays 일 이전 CSV 삭제 (§C purge).
* 2026-08-04: LOG_STORAGE_ANALYSIS Priority 3 — 시간당 40MB 누적 방지.
*/
fun purgeOldSessions(keepDays: Int = 30) {
try {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_YEAR, -keepDays)
val cutoff = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(calendar.time)
val dir = getDir()
dir.listFiles { f -> f.extension == "csv" }?.forEach { file ->
val prefix = file.nameWithoutExtension.take(10)
if (prefix < cutoff) file.delete()
}
} catch (_: Exception) { }
}
}