fix(labdb): 600·601 의 sensor.imu 를 labdb 표준 모양(샘플 하나)으로 — CSV 내보내기에 IMU 가 비어 있었다

labdb 에서 CSV 로 받으면 ax..gz 열이 빈칸이고 raw JSON 에는 IMU 가 있다(2026-09-29
보고). 사이트의 파싱 문제가 아니다 — labdb 표준(labdb.md "000" 레코드)은
`sensor.imu` 가 샘플 하나 `{ax,ay,az,gx,gy,gz}` 이고 CSV 내보내기는 그 여섯 값을
편다. 병원 임상(600)·정렬(601)은 2026-09-21 부터 거기에 회차 샘플 전부(배열)를
넣고 있었다. 사내 임상(001)은 처음부터 표준대로 보내고 있었다.

  · LabdbSensor.of(samples): imu = 마지막 샘플 하나(표준·CSV 에 나옴),
    imu_samples = 전부(FIFO 순), imu_sample_count. 없으면 빈 객체(0 으로 안 채움).
    001 의 LabdbUploader·tools/labdb_upload.py 와 같은 모양.
  · HospitalLabdbPayload·AlignLabdbPayload 가 이것을 쓴다.
  · tools/align_analyze.py 는 imu_samples 를 우선 읽고, 09-21~28 분(imu 가 배열)도
    그대로 받는다.
  · docs/LABDB_DATATYPES.md §IMU 를 새 모양으로. 09-21~28 업로드분은 CSV 에서
    IMU 가 비는 이유와, 내보내기에서 배열이면 마지막 원소를 쓰는 선택 사항을 적었다.

시험: LabdbSensorTest 3건 신규, Align/Hospital 페이로드 시험을 새 모양으로
(services.labdb 38건 통과). 전체 168건 중 실패 3건은 옛 세션 임시 경로에 쓰는
덤프 시험(Cm3PerTraceDump·GoldenDump·PrecisionDump)으로 이 변경과 무관하다.
병원 폰(SM-A155N)에 설치 완료.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 09:26:41 +09:00
parent 93b5726f21
commit 4fc40219f2
8 changed files with 155 additions and 28 deletions
@@ -42,12 +42,14 @@ import java.util.Locale
* phase : "sweep" | "confirm" ← 2026-09-18 추가
* cycle_idx : 그 위치 안에서의 순번
* channels : 6 × {ch, peak, peakIdx, data[~100]}
* sensor : {} 또는 {imu: [{ax,ay,az,gx,gy,gz}, …]} ← 2026-09-21 추가
* sensor : {} 또는 {imu:{ax..gz}, imu_samples:[…], imu_sample_count}
* ← 2026-09-21 추가 · 2026-09-29 표준 모양([LabdbSensor])
* ```
*
* ## IMU
* `align_{n}cm[_confirm]_imu.csv` 가 있으면 cycle 별로 `sensor.imu` 에 싣는다. 없으면
* `sensor` 는 빈 객체다 — **부재가 곧 "그때는 안 쟀다"** 이므로 0 으로 채우지 않는다.
* `align_{n}cm[_confirm]_imu.csv` 가 있으면 cycle 별로 `sensor` 에 싣는다([LabdbSensor.of]
* — `imu` 는 마지막 샘플 하나, `imu_samples` 는 전부). 없으면 `sensor` 는 빈 객체다 —
* **부재가 곧 "그때는 안 쟀다"** 이므로 0 으로 채우지 않는다.
* 정렬은 사람이 프로브를 옮겨 가며 재는 과정이라, 어느 위치의 파형이 이상할 때 그것이
* 자리 탓인지 그 순간 흔들린 탓인지 IMU 없이는 가를 수 없다.
*
@@ -152,9 +154,7 @@ object AlignLabdbPayload {
put("align_cm", cm)
put("phase", phase)
put("cycle_idx", ci)
put("sensor", JSONObject().apply {
imuByCycle[ci]?.let { put("imu", it) }
})
put("sensor", LabdbSensor.of(imuByCycle[ci]))
put("commandType", "MTB")
put("channels", chans)
})
@@ -40,10 +40,14 @@ import java.util.TimeZone
* 배터리·온도는 병원 CSV 에 열이 없다. `sensor` 에 넣지 않는다 — 0 이나 null 로 채우면
* 나중에 "배터리가 0 이었다"로 읽힌다.
*
* ## IMU (2026-09-21 추가)
* `*_imu.csv` 형제 파일이 있으면 `sensor.imu` 로 싣는다. 없으면 `sensor` 는 종전대로
* 빈 객체다 — **이 필드의 부재가 곧 "그때는 안 쟀다"** 라서, 없는 회차를 0 으로 채우면
* 안 된다. 예전 업로드분에는 애초에 파일이 없다.
* ## IMU (2026-09-21 추가 · 2026-09-29 표준 모양)
* `*_imu.csv` 형제 파일이 있으면 회차별로 `sensor` 에 싣는다([LabdbSensor.of] — `imu` 는
* 마지막 샘플 하나, `imu_samples` 는 전부). 없으면 `sensor` 는 종전대로 빈 객체다 —
* **이 필드의 부재가 곧 "그때는 안 쟀다"** 라서, 없는 회차를 0 으로 채우면 안 된다.
* 예전 업로드분에는 애초에 파일이 없다.
*
* 09-21~09-28 업로드분은 `sensor.imu` 가 **배열**이었다. labdb 표준은 샘플 하나라 CSV
* 내보내기의 `ax..gz` 가 비어 나왔다 — raw JSON 에는 다 있다.
*
* 같은 자세로 20회 반복하는 프로토콜이라, 어떤 회차만 값이 튈 때 환자가 움직인 것인지
* 알고리즘 문제인지 가르는 근거가 IMU 다.
@@ -133,10 +137,8 @@ object HospitalLabdbPayload {
put("rowIndex", rep)
put("datetime", iso(times[rep]))
put("commandType", "MTB")
// IMU 가 있으면 `sensor.imu`, 없으면 빈 객체(= 안 쟀다).
put("sensor", JSONObject().apply {
imuByRepeat[rep]?.let { put("imu", it) }
})
// IMU 가 있으면 표준 모양(imu · imu_samples), 없으면 빈 객체(= 안 쟀다).
put("sensor", LabdbSensor.of(imuByRepeat[rep]))
put("channels", chans)
})
}
@@ -0,0 +1,46 @@
/*
* LabdbSensor — labdb 레코드의 `sensor` 필드를 **표준 모양**으로 만든다.
*
* labdb 표준(labdb.md `"000"` 레코드 · §5-6 CSV 내보내기)은 `sensor.imu` 가 **샘플 하나**
* `{ax,ay,az,gx,gy,gz}` 다. CSV 내보내기는 그 여섯 값을 `ax..gz` 열로 편다 — 레코드당
* 값 하나라 배열은 펼 수 없다.
*
* 병원 임상(600)·정렬(601)은 2026-09-21 부터 `sensor.imu` 에 **회차 샘플 전부(배열)** 를
* 넣고 있었다. 서버는 JSON 을 그대로 저장하니 raw 내보내기에는 다 보였지만, CSV 의
* `ax..gz` 는 빈칸이었다(2026-09-29 발견 — 분석팀이 CSV 로 받으면 IMU 가 없다).
* 사이트의 파싱 잘못이 아니라 우리가 표준을 어긴 것이다.
*
* 그래서 사내 임상(001, [LabdbUploader.cycleToRecord]·`tools/labdb_upload.py`)이 처음부터
* 쓰던 모양으로 셋을 맞춘다:
*
* imu : 마지막(가장 최근 = piezo 캡처 직전) 샘플 하나 — 표준. CSV 에 나온다.
* imu_samples : 회차 샘플 전부(FIFO 순). raw JSON 에서 그대로. 기울기 재계산은 이것으로.
* imu_sample_count : 그 개수.
*
* 샘플이 없으면 **빈 객체** — 부재가 곧 "그때는 안 쟀다" 다. 0 으로 채우지 않는다.
*/
package com.medithings.vesiscan.services.labdb
import org.json.JSONArray
import org.json.JSONObject
object LabdbSensor {
private val AXES = listOf("ax", "ay", "az", "gx", "gy", "gz")
/**
* @param samples `[{ax,ay,az,gx,gy,gz}, …]` FIFO 순(마지막이 최신). null·빈 배열이면 빈 객체.
*/
fun of(samples: JSONArray?): JSONObject {
val out = JSONObject()
if (samples == null || samples.length() == 0) return out
val latest = samples.optJSONObject(samples.length() - 1) ?: return out
out.put("imu", JSONObject().apply {
// 없는 축은 빼 둔다 — 0 을 넣으면 "0 이었다"가 된다.
for (k in AXES) if (latest.has(k)) put(k, latest.getDouble(k))
})
out.put("imu_samples", samples)
out.put("imu_sample_count", samples.length())
return out
}
}