Clinical: measurement.json 통합 저장 (사이클 누적 + End 시점 finalize)
ClinicalSessionStore에 MeasurementCycle 누적 버퍼 추가. End Session 시 6채널 piezo + 15 IMU 샘플을 단일 measurement.json으로 묶어 저장하여 분석 PC에서 한 줄(json.load) 로드 가능. 기존 CSV 로거는 유지. ClinicalLiveView는 pendingPiezo 패턴으로 piezo(reb×6+raa)와 imu(rim)를 한 cycle로 묶어 addCycle() 호출. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,24 @@ package com.example.medilightv2android.services
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import com.example.medilightv2android.ble.ImuSample
|
||||
import com.example.medilightv2android.ble.PiezoChannelData
|
||||
import com.example.medilightv2android.models.ClinicalSession
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/** 한 측정 사이클 = mtb 응답 1회 = 6채널 piezo + 15 IMU 샘플. */
|
||||
data class MeasurementCycle(
|
||||
val cycle: Int,
|
||||
val timestampMs: Long,
|
||||
val piezoChannels: Map<Int, List<Int>>, // channel idx → samples
|
||||
val imuSamples: List<ImuSample>
|
||||
)
|
||||
|
||||
/**
|
||||
* 임상 측정 단위(한 측정 = 한 라벨 = 한 폴더) 전역 store.
|
||||
*
|
||||
@@ -41,6 +52,9 @@ object ClinicalSessionStore {
|
||||
var lastTrueVolumeMl: String = "" // 문자열로 보관 (입력 폼과 동일 형식)
|
||||
var lastAbdomenThicknessMm: String = ""
|
||||
|
||||
/** End 시점에 measurement.json 통합 생성용 — 측정 중 사이클 누적 */
|
||||
private val cyclesBuffer = mutableListOf<MeasurementCycle>()
|
||||
|
||||
fun startMeasurement(context: Context, session: ClinicalSession) {
|
||||
currentSession = session
|
||||
lastExaminer = session.examiner
|
||||
@@ -48,10 +62,25 @@ object ClinicalSessionStore {
|
||||
lastSubjectId = session.subjectId
|
||||
lastTrueVolumeMl = session.trueVolumeMl?.toString() ?: ""
|
||||
lastAbdomenThicknessMm = session.abdomenThicknessMm?.toString() ?: ""
|
||||
cyclesBuffer.clear()
|
||||
currentLogDir()?.mkdirs()
|
||||
writeMeta(context)
|
||||
}
|
||||
|
||||
/** 한 사이클(piezo + IMU) 누적 — End 시 measurement.json에 합쳐짐 */
|
||||
fun addCycle(piezo: List<PiezoChannelData>, imu: List<ImuSample>) {
|
||||
if (currentSession == null) return
|
||||
cyclesBuffer.add(MeasurementCycle(
|
||||
cycle = cyclesBuffer.size,
|
||||
timestampMs = System.currentTimeMillis(),
|
||||
piezoChannels = piezo.sortedBy { it.channel }
|
||||
.associate { it.channel to it.buffer.map { s -> s.toInt() } },
|
||||
imuSamples = imu.toList(),
|
||||
))
|
||||
}
|
||||
|
||||
val cycleCount: Int get() = cyclesBuffer.size
|
||||
|
||||
/** 현재 측정의 폴더 (= 모든 로거의 저장 디렉토리). */
|
||||
fun currentLogDir(): File? {
|
||||
val s = currentSession ?: return null
|
||||
@@ -110,17 +139,95 @@ object ClinicalSessionStore {
|
||||
File(dir, "meta.json").writeText(meta.toString(2))
|
||||
}
|
||||
|
||||
/** 측정 종료: endedAt 기록 → meta.json 갱신 → currentSession=null */
|
||||
/** 측정 종료: endedAt → meta.json 갱신 → measurement.json 통합 생성 → currentSession=null */
|
||||
fun endMeasurement(context: Context): File? {
|
||||
val s = currentSession ?: return null
|
||||
s.endedAt = System.currentTimeMillis()
|
||||
writeMeta(context)
|
||||
writeMeasurementJson(context)
|
||||
val dir = currentLogDir()
|
||||
lastFinalizedFolder = dir
|
||||
currentSession = null
|
||||
cyclesBuffer.clear()
|
||||
return dir
|
||||
}
|
||||
|
||||
/**
|
||||
* End Session 시점에 모든 사이클을 단일 measurement.json으로 통합.
|
||||
* 분석 PC에서 한 줄 로드: data = json.load(open('measurement.json'))
|
||||
*/
|
||||
private fun writeMeasurementJson(context: Context) {
|
||||
val s = currentSession ?: return
|
||||
val dir = currentLogDir() ?: return
|
||||
dir.mkdirs()
|
||||
try {
|
||||
val root = JSONObject()
|
||||
|
||||
// meta: writeMeta()와 동일 내용 포함 (단독 파일로 자족 가능)
|
||||
val meta = JSONObject().apply {
|
||||
put("subject_id", s.subjectId)
|
||||
put("device_name", s.deviceName)
|
||||
put("posture", s.posture.name)
|
||||
put("step", s.step.name)
|
||||
put("is_alignment", s.step.isAlignment)
|
||||
put("examiner", s.examiner)
|
||||
put("notes", s.notes)
|
||||
put("label", s.label)
|
||||
s.trueVolumeMl?.let { put("true_volume_ml", it) }
|
||||
s.abdomenThicknessMm?.let { put("abdomen_thickness_mm", it) }
|
||||
put("started_at", s.startedAt)
|
||||
put("started_at_iso",
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(Date(s.startedAt)))
|
||||
s.endedAt?.let {
|
||||
put("ended_at", it)
|
||||
put("ended_at_iso",
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(Date(it)))
|
||||
put("duration_ms", it - s.startedAt)
|
||||
}
|
||||
put("captured_cycles", cyclesBuffer.size)
|
||||
put("app_version_name", try {
|
||||
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "unknown"
|
||||
} catch (_: Exception) { "unknown" })
|
||||
val ble = com.example.medilightv2android.ble.BleManager.current
|
||||
ble?.firmwareVersion?.value?.takeIf { it.isNotEmpty() }?.let { put("firmware_version", it) }
|
||||
ble?.hardwareVersion?.value?.takeIf { it.isNotEmpty() }?.let { put("hardware_version", it) }
|
||||
ble?.serialNumber?.value?.takeIf { it.isNotEmpty() }?.let { put("serial_number", it) }
|
||||
}
|
||||
root.put("meta", meta)
|
||||
|
||||
// cycles: 모든 사이클을 piezo + imu 묶음으로
|
||||
val cyclesArr = JSONArray()
|
||||
for (c in cyclesBuffer) {
|
||||
val piezo = JSONObject()
|
||||
c.piezoChannels.forEach { (ch, samples) ->
|
||||
piezo.put("CH$ch", JSONArray(samples))
|
||||
}
|
||||
val imuArr = JSONArray()
|
||||
for (sample in c.imuSamples) {
|
||||
imuArr.put(JSONObject().apply {
|
||||
put("ax", sample.ax.toDouble())
|
||||
put("ay", sample.ay.toDouble())
|
||||
put("az", sample.az.toDouble())
|
||||
put("gx", sample.gx.toDouble())
|
||||
put("gy", sample.gy.toDouble())
|
||||
put("gz", sample.gz.toDouble())
|
||||
})
|
||||
}
|
||||
cyclesArr.put(JSONObject().apply {
|
||||
put("cycle", c.cycle)
|
||||
put("timestamp_ms", c.timestampMs)
|
||||
put("piezo", piezo)
|
||||
put("imu", imuArr)
|
||||
})
|
||||
}
|
||||
root.put("cycles", cyclesArr)
|
||||
|
||||
File(dir, "measurement.json").writeText(root.toString(2))
|
||||
} catch (_: Exception) {
|
||||
// 저장 실패해도 측정 자체는 막지 않음
|
||||
}
|
||||
}
|
||||
|
||||
/** 측정 폐기 (메타/파일은 그대로 두되 store만 비움) */
|
||||
fun discardMeasurement() {
|
||||
currentSession = null
|
||||
|
||||
Reference in New Issue
Block a user