feat: Sensor Alignment에 IMU 통합 (mtb? + rim: 정식 처리)
PPT 'VesiScan-Basic IMU FW 진행사항_260521' 사양 기반 구현.
실측 1회 (2026-05-27 12:24, 평평한 책상)로 endian/필드 순서/시간 순서 확정 후 정식 적용.
확정 사양 (실측):
- rim: 패킷 188B = [tag 4B "rim:"] [num 2B BE] [data 180B = 15 samples × 12B] [crc 2B]
- 1 sample = 12B: Ax 2B + Ay 2B + Az 2B + Gx 2B + Gy 2B + Gz 2B (모두 BE int16)
- Accel ±4g sensitivity: /8192 g/LSB
- Gyro ±500 dps sensitivity: /65.536 dps/LSB
- 샘플 순서: samples[0] = oldest (FIFO 순)
신규 파일:
- ble/ImuPacketCollector.kt — rim: 파서. ImuSample data class + 15-sample list 콜백
- services/ImuCsvLogger.kt — Clinical 모드 한정 imu.csv 자동 저장
컬럼: scan_id, timestamp, clinical_label, firmware_version, sample_idx, ax_g, ay_g, az_g, gx_dps, gy_dps, gz_dps
- managers/ImuAnalysis.kt — 자세 추정/Tilt/흔들림 RMS 유틸 (간이, 임상 보정 필요)
수정:
- BleManager: imuCollector 필드 + sendMtb() 정식 명령
(canSendMaa() 게이트 maa와 공유. piezo + IMU 동시 캡처)
- BleManager: rim: 핸들러 정식화 (imuCollector.parseRim 위임)
- PlacementGuideView: sendAllChannels → sendMtb 교체, imuCollector.onComplete 등록
Clinical 모드일 때 ImuCsvLogger.log() 자동 호출
Dev 모드 한정 상단 칩에 "IMU: Supine · tilt=N° · shake=N.Ndps" 표시
데이터 흐름:
performScan → sendMtb? → 응답:
1) reb × 6 (Piezo, 기존 처리)
2) raa (Piezo end, 기존 onMultiChannelComplete 트리거)
3) rim (IMU, 신규 imuCollector.onComplete → lastImuSamples 갱신 + imu.csv)
분석:
- ImuAnalysis.estimatePosture: Accel 평균으로 Supine/Sitting/Standing/Tilted/Moving 판정
- shakeRmsDps > 30 → "Moving" (자세 판정 보류)
- tiltDegrees: z축 vs 중력 각도
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package com.example.medilightv2android.services
|
||||
|
||||
import com.example.medilightv2android.ble.ImuSample
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* IMU 샘플을 CSV로 저장. 임상 R&D 모드 한정.
|
||||
*
|
||||
* 파일 위치 (임상 모드일 때):
|
||||
* Downloads/VesiScan_Sessions/{folder}/imu.csv
|
||||
*
|
||||
* 한 번의 mtb? 응답에서 15 샘플 → CSV 15행. scan_id로 같은 측정의 piezo adc.csv와 매칭 가능.
|
||||
*
|
||||
* Sensitivity:
|
||||
* ax/ay/az: g (Accel ±4g, 1 LSB = 1/8192 g)
|
||||
* gx/gy/gz: dps (Gyro ±500 dps, 1 LSB = 1/65.536 dps)
|
||||
*
|
||||
* Sample 순서: 한 응답 안에서 oldest→newest. sample_idx 컬럼으로 명시.
|
||||
*/
|
||||
object ImuCsvLogger {
|
||||
|
||||
private val timeFmt = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
|
||||
|
||||
/**
|
||||
* 임상 모드 한정. ClinicalSessionStore.currentLogDir()가 있을 때만 저장.
|
||||
* samples를 oldest→newest 순서로 그대로 기록.
|
||||
*/
|
||||
fun log(samples: List<ImuSample>) {
|
||||
val dir = ClinicalSessionStore.currentLogDir() ?: return
|
||||
if (samples.isEmpty()) return
|
||||
try {
|
||||
dir.mkdirs()
|
||||
val file = File(dir, "imu.csv")
|
||||
val isNew = !file.exists() || file.length() == 0L
|
||||
val scanId = AdcCsvLogger.lastScanId // piezo adc.csv와 매칭용
|
||||
val timestamp = timeFmt.format(Date())
|
||||
val clinicalLabel = ClinicalSessionStore.currentLabel()
|
||||
val fwVersion = com.example.medilightv2android.ble.BleManager.current?.firmwareVersion?.value ?: ""
|
||||
|
||||
file.bufferedWriter().use { _ -> } // ensure exists
|
||||
java.io.FileWriter(file, !isNew).buffered().use { writer ->
|
||||
if (isNew) {
|
||||
writer.write("scan_id,timestamp,clinical_label,firmware_version,sample_idx,ax_g,ay_g,az_g,gx_dps,gy_dps,gz_dps")
|
||||
writer.newLine()
|
||||
}
|
||||
for ((idx, s) in samples.withIndex()) {
|
||||
writer.write(buildString {
|
||||
append("$scanId,$timestamp,$clinicalLabel,$fwVersion,$idx,")
|
||||
append("%.4f".format(s.ax)); append(",")
|
||||
append("%.4f".format(s.ay)); append(",")
|
||||
append("%.4f".format(s.az)); append(",")
|
||||
append("%.4f".format(s.gx)); append(",")
|
||||
append("%.4f".format(s.gy)); append(",")
|
||||
append("%.4f".format(s.gz))
|
||||
})
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// 로그는 측정 자체를 막으면 안 됨 — silent fail
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user