feat(clinical): 병원 임상 측정 모드 — 주파수×cycle 6조합 자동 순회
## 조작자가 하는 일은 하나다
간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도만** 고르고
[측정 시작]을 누른다. 이후 주파수 2종 × cycle 3종 = 6조합을 앱이 자동으로 순회하며
각 조합마다 n회(기본 20) 측정하고 조합별 파일로 저장한다.
조합을 손으로 바꾸게 두면 반드시 빠뜨리거나 잘못 기록한다. 이미 채워 둔 방광은 다시
만들 수 없으니, 그 자리에서 놓친 조합은 그날 데이터에서 영영 빈다. 사람이 개입하는
지점을 하나로 줄인 것이 이 모드의 핵심이다.
진입: 홈에서 캐릭터 3연타 → 개발자 모드 → [병원 임상 측정]
## 프로토콜
자세 Supine / Sitting / Standing (기존 ClinicalPosture 재사용)
주파수 1.8 · 2.3 MHz → mpa? 첫 인자
cycle 3 · 5 · 7 → mpa? 둘째 인자
방광 채움 0/20/40/60/80/100 % → 사람이 아는 값 = 정답 라벨
반복 화면 입력 (기본 20)
## 저장
Downloads/VesiScan_Hospital/{날짜_환자}/
2026-09-03_홍길동_Supine_040pct_1.8MHz-fopt1_c3.csv
run_HHmmss.json ← 계획 대비 실제 저장 수
파일명에 조건을 전부 적는다. 폴더로만 구분하면 파일 하나를 옮기는 순간 조건을 잃는데,
임상 데이터는 나중에 다른 사람이 모아서 분석한다. CSV 열 구성은 기존 AdcCsvLogger 와
맞춰(scan_id·timestamp·channel·s0~s99) 분석 스크립트를 새로 만들지 않아도 되게 했다.
기존 임상 R&D(`VesiScan_Sessions/`)와 폴더를 나눴다 — 목적도 구조도 달라서 섞이면
나중에 파일을 하나씩 열어 봐야 한다.
## ⚠ 주파수 코드값이 확인 전이다
`mpa?` 첫 인자는 정수인데 그 정수와 MHz 의 대응이 코드에도
docs/BLE_PROTOCOL_REFERENCE.md 에도 없다. 기존 코드는 늘 `freqOption = 2` 만 썼다
(PlacementGuideView · MeasurementService). 그래서 1.8→1, 2.3→2 는 **가정**이다.
확인 전에 모은 데이터가 버려지지 않도록, 실제로 보낸 정수를 파일명(`fopt1`)과 CSV 열
(`freq_option`), run json 에 함께 남긴다. 대응이 반대로 밝혀져도 라벨만 바꾸면 된다.
run json 에 `freq_option_mapping_confirmed: false` 를 박아 두어 나중에 이 데이터가
어떤 상태에서 모였는지 알 수 있게 했다. 화면에도 같은 경고를 띄운다.
## 데이터 정합성
결과를 평범한 var 로 주고받으면 BLE 스레드↔코루틴 간 가시성이 보장되지 않아 Channel
을 쓴다. 그리고 **회차마다 보내기 전에 채널을 비운다** — 직전 회차가 시간초과된 뒤
뒤늦게 도착한 결과가 남아 있으면 이번 회차 데이터로 잘못 기록된다.
한 회차가 3초 안에 안 오면 실패로 세고 다음으로 넘어간다. 한 번 막혔다고 전체가
멈추면 채워 둔 방광을 버리게 된다. 실패 수는 화면과 run json 에 남는다.
## 검증
빌드 통과. **실기기 확인은 아직 못 했다** — 폰이 절전 상태로 들어가 화면을 못 띄웠고,
프로브 연결 상태의 측정 루프는 전혀 돌려보지 못했다. 내일 임상 전에 반드시 한 번
돌려봐야 한다(6조합 × 20회 = 120측정 · 약 2~3분 예상).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* HospitalProtocol — 병원 임상 측정 프로토콜 정의.
|
||||
*
|
||||
* ## 무엇을 하는 모드인가
|
||||
* 간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도 하나만** 고르고
|
||||
* [측정 시작]을 누른다. 그 뒤로는 앱이 주파수 2종 × cycle 3종 = **6조합을 자동으로
|
||||
* 순회**하며 각 조합마다 6채널을 n회 측정하고, 조합별로 파일 하나씩 저장한다.
|
||||
*
|
||||
* 사람이 개입하는 지점을 방광 채움 하나로 줄인 것이 핵심이다 — 조합을 손으로 바꾸면
|
||||
* 반드시 빠뜨리거나 잘못 기록한다. 6조합 × n회가 한 번의 버튼으로 끝난다.
|
||||
*
|
||||
* ## ⚠ 주파수 코드값이 아직 확인되지 않았다 (2026-09-02)
|
||||
* 펌웨어에 보내는 `mpa?` 명령의 첫 인자는 **정수**인데, 그 정수와 실제 MHz 의 대응이
|
||||
* 코드에도 `docs/BLE_PROTOCOL_REFERENCE.md` 에도 적혀 있지 않다. 기존 코드는 항상
|
||||
* `freqOption = 2` 만 쓰고 있었다(PlacementGuideView · MeasurementService).
|
||||
*
|
||||
* 그래서 [ProbeFrequency.freqOption] 값은 **가정**이다. 펌웨어팀 확인 후 이 표 한 줄만
|
||||
* 고치면 된다.
|
||||
*
|
||||
* 확인 전에 모은 데이터가 버려지지 않도록, 저장 파일명과 meta 에 **실제로 보낸 정수**를
|
||||
* `fopt{N}` 으로 함께 남긴다. 대응이 반대로 밝혀져도 라벨만 바꿔 되살릴 수 있다.
|
||||
*/
|
||||
package com.medithings.vesiscan.models
|
||||
|
||||
/** 프로브 송신 주파수. [freqOption] 이 `mpa?` 의 첫 인자로 나간다. */
|
||||
enum class ProbeFrequency(val label: String, val freqOption: Int) {
|
||||
// ⚠ freqOption 값은 펌웨어팀 확인 전의 가정이다. 위 KDoc 참고.
|
||||
F_1_8("1.8", 1),
|
||||
F_2_3("2.3", 2), // 기존 코드가 늘 쓰던 값이 2 다.
|
||||
;
|
||||
|
||||
/** 파일명에 쓸 표기 — MHz 와 실제 보낸 정수를 함께 남긴다. */
|
||||
val fileTag: String get() = "${label}MHz-fopt$freqOption"
|
||||
}
|
||||
|
||||
/** 버스트 cycle 수. `mpa?` 의 둘째 인자로 그대로 나간다. */
|
||||
enum class ProbeCycle(val cycles: Int) {
|
||||
C3(3), C5(5), C7(7);
|
||||
|
||||
val fileTag: String get() = "c$cycles"
|
||||
}
|
||||
|
||||
/**
|
||||
* 방광 채움 정도 — 간호사가 채워 둔 값을 조작자가 고른다.
|
||||
*
|
||||
* 앱이 재는 값이 아니라 **사람이 아는 값**이다. 이 모드의 목적이 "채움 정도가 알려진
|
||||
* 상태에서 6채널 원신호를 모으는 것"이라, 이 값이 곧 정답 라벨이 된다.
|
||||
*/
|
||||
enum class BladderFill(val percent: Int) {
|
||||
P0(0), P20(20), P40(40), P60(60), P80(80), P100(100);
|
||||
|
||||
val label: String get() = "$percent%"
|
||||
val fileTag: String get() = "%03dpct".format(percent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 한 번의 [측정 시작]이 도는 조합 목록 — 주파수 2 × cycle 3 = 6.
|
||||
*
|
||||
* 순서를 고정해 둔다. 매번 같은 순서로 돌아야 나중에 파일을 비교할 때 조건이 섞이지
|
||||
* 않는다. 주파수를 바깥 고리에 둔 이유는 주파수 전환이 cycle 전환보다 프로브에 더 큰
|
||||
* 변화라, 한 주파수 안에서 cycle 셋을 몰아 끝내는 편이 안정적이기 때문이다.
|
||||
*/
|
||||
val HOSPITAL_COMBINATIONS: List<Pair<ProbeFrequency, ProbeCycle>> =
|
||||
ProbeFrequency.entries.flatMap { f -> ProbeCycle.entries.map { c -> f to c } }
|
||||
|
||||
/** 한 조합당 기본 반복 횟수. 화면에서 바꿀 수 있다. */
|
||||
const val HOSPITAL_DEFAULT_REPEATS = 20
|
||||
Reference in New Issue
Block a user