From a58c9270661a860d30d6521e6c9de58799de3da3 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Thu, 3 Sep 2026 12:23:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(clinical):=20=ED=94=84=EB=A1=9C=EB=B8=8C?= =?UTF-8?q?=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(mcs=20=EC=88=98=EB=8F=99=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주파수(1.8/2.3) · cycle(3/5/7) 을 골라 "기기에 주입"으로 프로브에 써 넣는다. 병원 임상 모드 화면 아래, mcs 영구저장 경고가 있는 자리에 둔다. ## 왜 필요한가 프로토콜은 6조합을 자동으로 돌지만 **그 밖의 측정은 프로브에 마지막으로 남은 설정으로 돈다.** 프로토콜을 돌리면 2.3MHz·cycle 7 이, 정렬을 하면 2.3MHz·cycle 3 이 남는다. 원하는 조건으로 되돌리거나 특정 조건만 시험하려면 손으로 넣을 길이 있어야 한다는 현장 요구. ## echo 를 보고 나서 성공이라고 말한다 설정이 거부돼도 프로브는 옛 설정으로 측정을 계속한다. 그래서 `rcs:` 응답 값이 요청과 같은지까지 확인하고, 결과를 네 갈래로 구분해 보여 준다: 적용됨 / 응답 없음 / 거부됨(펌웨어 사유) / 값 불일치(프로브에 남은 실제 값 표시) 프로토콜 루프가 조합마다 하는 검사와 같은 기준이다. avg 10 · delay 10µs · samples 100 은 고정이며 화면에 명시한다. 측정 중에는 비활성 — 루프가 조합을 바꿔 가며 쓰는 중에 끼어들면 라벨과 데이터가 어긋난다. Co-Authored-By: Claude Opus 5 --- .../ui/views/clinical/HospitalModeView.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt index 46e4ac4..098026d 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt @@ -47,6 +47,7 @@ import com.medithings.vesiscan.ui.theme.* import androidx.compose.foundation.border import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import org.json.JSONArray @@ -376,6 +377,9 @@ fun HospitalModeView(appState: AppState) { } } + Spacer(Modifier.height(20.dp)) + ProbeParamCard(bleManager = bleManager, enabled = !running) + Spacer(Modifier.height(16.dp)) Text( "저장 위치: Downloads/VesiScan_Hospital/", @@ -551,3 +555,100 @@ private fun AnchorCard(anchorCm: Int?, enabled: Boolean, onAlign: () -> Unit) { ) } } + +/** + * 프로브 파라미터 수동 주입 — 주파수·cycle 을 골라 `mcs` 로 직접 써 넣는다. + * + * ## 왜 필요한가 + * 프로토콜은 6조합을 자동으로 돌지만, 그 밖의 측정(일반 측정·정렬 외 실험)은 프로브에 + * **마지막으로 남은 설정**으로 돈다. 프로토콜을 돌리고 나면 2.3MHz·cycle 7 이 남고, + * 정렬을 하면 2.3MHz·cycle 3 이 남는다. 원하는 조건으로 되돌리거나 특정 조건만 + * 시험하려면 손으로 넣을 길이 있어야 한다. + * + * ## echo 를 확인하고 나서 성공이라고 말한다 + * 설정이 거부돼도 프로브는 옛 설정으로 측정을 계속한다. 응답(`rcs:`)의 값이 요청과 + * 같은지까지 봐야 "들어갔다"고 할 수 있다 — 프로토콜 루프가 조합마다 하는 검사와 같다. + */ +@Composable +private fun ProbeParamCard(bleManager: BleManager, enabled: Boolean) { + val scope = rememberCoroutineScope() + var freq by remember { mutableStateOf(ProbeFrequency.F_2_3) } + var cycle by remember { mutableStateOf(ProbeCycle.C3) } + var busy by remember { mutableStateOf(false) } + var result by remember { mutableStateOf(null) } + var ok by remember { mutableStateOf(false) } + val connected by bleManager.isConnected + + Column( + Modifier.fillMaxWidth() + .background(Color.White, RoundedCornerShape(12.dp)) + .padding(12.dp) + ) { + Text("프로브 파라미터 직접 설정", fontSize = 15.sp, fontWeight = FontWeight.Bold) + Text( + "고른 값을 프로브에 써 넣습니다. avg ${HospitalFixedParams.AVG} · " + + "delay ${HospitalFixedParams.DELAY_US}µs · samples ${HospitalFixedParams.SAMPLES} 는 고정입니다.", + fontSize = 11.sp, color = MlSecondaryText, + modifier = Modifier.padding(top = 2.dp, bottom = 8.dp), + ) + + Label("주파수") + ChipRow(ProbeFrequency.entries, freq, { freq = it }, enabled && !busy) { + "${it.label}MHz" + } + Spacer(Modifier.height(10.dp)) + Label("cycle") + ChipRow(ProbeCycle.entries, cycle, { cycle = it }, enabled && !busy) { + "${it.cycles}" + } + Spacer(Modifier.height(12.dp)) + + Button( + onClick = { + busy = true; result = null + scope.launch { + bleManager.piezoConfigEcho.value = null + bleManager.sendPiezoConfig( + freq = freq.freqOption, + cycles = cycle.cycles, + avg = HospitalFixedParams.AVG, + delayUs = HospitalFixedParams.DELAY_US, + samples = HospitalFixedParams.SAMPLES, + ) + val echo = withTimeoutOrNull(CONFIG_TIMEOUT_MS) { + while (bleManager.piezoConfigEcho.value == null) delay(20) + bleManager.piezoConfigEcho.value + } + ok = echo?.matches( + freq.freqOption, cycle.cycles, HospitalFixedParams.AVG, + HospitalFixedParams.DELAY_US, HospitalFixedParams.SAMPLES, + ) == true + result = when { + ok -> "적용됨 — ${freq.label}MHz · cycle ${cycle.cycles} (프로브에 저장)" + echo == null -> "응답 없음 — 연결과 프로브 상태를 확인하세요." + echo.error != null -> "거부됨 — ${echo.error}" + else -> "값 불일치 — 프로브는 ${echo.freq}/${echo.cycles} 로 남았습니다." + } + busy = false + } + }, + enabled = enabled && !busy && connected, + modifier = Modifier.fillMaxWidth().height(46.dp), + shape = RoundedCornerShape(12.dp), + ) { + Text(if (busy) "주입 중…" else "기기에 주입", fontWeight = FontWeight.Bold) + } + + result?.let { + Text( + it, fontSize = 12.sp, + color = if (ok) MlSuccess else MlCritical, + modifier = Modifier.padding(top = 8.dp), + ) + } + if (!connected) { + Text("기기를 먼저 연결하세요.", fontSize = 11.sp, color = MlSecondaryText, + modifier = Modifier.padding(top = 6.dp)) + } + } +}