fix(clinical): 연결이 끊기면 기기에서 온 값을 비운다
연결 해제를 눌러도 앞 프로브의 값이 화면에 그대로 남아 있었다.
방광 용적 187 mL ← 끊긴 프로브의 값인데 "방광 용적"으로 보인다
채널 파형 6개 ← 옛 신호
채널 접촉 CH2 떠 있음 ← 옛 접촉
수동 확인의 "마지막 측정" · 연속 대표값
끊기 버튼이 비우던 것은 anchorCm·anchorBasis 둘뿐이었다.
## 왜 위험한가
**프리셋이 다른 프로브로 갈아 끼우면 같은 신호가 전혀 다른 용적이 된다.**
PiezoHW.activePreset 이 dps·delay 를 정하고, 실측에서 같은 데이터가 125mL 와 490mL 로
갈렸다. R100/R200/R300 을 번갈아 쓰는 것이 이 화면의 정상 사용이라 — 끊기 버튼 주석이
그렇게 적고 있다 — 남은 값을 새 프로브의 값으로 읽는 일이 실제로 일어난다.
## 버튼이 아니라 isConnected 를 본다
프로브가 **스스로** 끊기는 경우가 있다(VBTFW0206 freeze, 2026-09-08 로그에서 advertising
까지 멈췄다). 버튼에만 넣으면 그때는 여전히 옛 값이 남는다. LaunchedEffect(isConnected)
한 곳에서 끊기는 모든 경우를 맡는다.
## 비우는 것과 남기는 것
비움 runBv · liveChannels · detachSnapshot/Watcher
수동 확인의 outcome · live · window · tick · mode
남김 환자명 · 자세 · 충만도 · 반복 · 복부 두께 (조작자가 고른 설정)
lastRunDir · pendingUploads (못 올린 파일을 다시 올릴 유일한 길)
직전 실행 요약 · configFailed ("직전 실행"이라 라벨이 정확하다)
수동 확인 저장 결과 문구 (남겼는지 의심하게 된다)
수동 확인은 모드도 내린다. 끊긴 채로 연속 측정이 돌면 3초마다 시간 초과만 쌓인다.
## 정렬 화면은 판정 기록을 지키되 경고한다
BV·파형·접촉은 같이 비우지만 **guide·records·last 는 비우지 않는다.** 한 위치가 20
cycle 인데 연결이 잠깐 끊겼다고 날리면 환자를 다시 눕혀야 한다. 재연결 후 이어서 재는
것이 정상 경로다.
다만 프로브를 **바꿔서** 연결하면 그 세션은 섞인 데이터가 된다 — align_result.json 의
hw_preset 은 한 번만 기록되므로 절반이 잘못 라벨링된다. 그 경우를 앱이 알 수 없으므로
경고 한 줄로 사람에게 묻는다: "같은 프로브면 이어서 재도 됩니다. 바꿨다면 [처음부터
다시 정렬]."
테스트 129개 통과.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,25 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
|
|
||||||
val done = last?.done == true
|
val done = last?.done == true
|
||||||
|
|
||||||
|
// ── 연결이 끊기면 기기에서 온 값을 비운다 ───────────────────────────────
|
||||||
|
// 병원 모드와 같은 이유다 — 끊긴 프로브의 BV·파형·접촉이 새 프로브의 것으로 읽힌다.
|
||||||
|
//
|
||||||
|
// 다만 **판정 기록(guide·records·last)은 비우지 않는다.** 여기까지 잰 위치가 한
|
||||||
|
// 위치당 20 cycle 인데, 연결이 잠깐 끊겼다고 날리면 그 환자를 다시 눕혀야 한다.
|
||||||
|
// 재연결 후 이어서 재는 것이 정상 경로다.
|
||||||
|
//
|
||||||
|
// 대신 프로브를 **바꿔서** 다시 연결하면 그 세션은 섞인 데이터가 된다 —
|
||||||
|
// align_result.json 의 hw_preset 은 한 번만 기록되므로 절반이 잘못 라벨링된다.
|
||||||
|
// 그때는 [처음부터 다시 정렬] 을 눌러야 한다. 화면이 그 사실을 말해 준다.
|
||||||
|
var disconnectedMidRun by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(isConnected) {
|
||||||
|
if (isConnected) return@LaunchedEffect
|
||||||
|
bv = null
|
||||||
|
liveChannels = emptyList()
|
||||||
|
detachWatcher.reset(); detachSnapshot = null
|
||||||
|
if (records.isNotEmpty()) disconnectedMidRun = true
|
||||||
|
}
|
||||||
|
|
||||||
// ── 떠날 때 정렬을 올린다 ───────────────────────────────────────────────
|
// ── 떠날 때 정렬을 올린다 ───────────────────────────────────────────────
|
||||||
// 종전에는 정렬 업로드가 **수동**이었다. 그 버튼의 원래 용도가 "파형이 이상할 때
|
// 종전에는 정렬 업로드가 **수동**이었다. 그 버튼의 원래 용도가 "파형이 이상할 때
|
||||||
// 개발자에게 보내기"(메모를 받는 다이얼로그)라, 정상적으로 정렬을 끝내면 601 이
|
// 개발자에게 보내기"(메모를 받는 다이얼로그)라, 정상적으로 정렬을 끝내면 601 이
|
||||||
@@ -623,6 +642,21 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 프로브를 바꿔 끼웠으면 이 세션을 이어 쓸 수 없다. hw_preset 이 한 번만
|
||||||
|
// 기록되므로 절반이 다른 기기의 데이터인데 한 기기로 라벨링된다.
|
||||||
|
if (disconnectedMidRun && isConnected) {
|
||||||
|
Text(
|
||||||
|
"측정 도중 연결이 끊겼습니다. 같은 프로브면 이어서 재도 됩니다. " +
|
||||||
|
"프로브를 바꿨다면 [처음부터 다시 정렬] 을 눌러 주세요 — " +
|
||||||
|
"기기마다 빔 각도와 프리셋이 달라 섞이면 기록이 잘못됩니다.",
|
||||||
|
fontSize = 12.sp, color = MlWarning,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.background(MlWarning.copy(alpha = 0.10f), RoundedCornerShape(8.dp))
|
||||||
|
.padding(10.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
Button(
|
Button(
|
||||||
onClick = { measuring = true },
|
onClick = { measuring = true },
|
||||||
@@ -751,6 +785,7 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
guide.reset(); cm = 0; last = null; records = emptyList(); error = null
|
guide.reset(); cm = 0; last = null; records = emptyList(); error = null
|
||||||
lateral.reset(); lrCommit = null; lrAccum = 0
|
lateral.reset(); lrCommit = null; lrAccum = 0
|
||||||
lrConfirmed = false; lrError = null; lrRunning = false
|
lrConfirmed = false; lrError = null; lrRunning = false
|
||||||
|
disconnectedMidRun = false
|
||||||
},
|
},
|
||||||
enabled = !lrRunning,
|
enabled = !lrRunning,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -125,6 +125,21 @@ fun BvMeasureSection(
|
|||||||
var saveFailed by remember { mutableStateOf(false) }
|
var saveFailed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
||||||
|
// 연결이 끊기면 이 구간의 값도 비운다. "마지막 측정"과 연속 대표값이 남아 있으면
|
||||||
|
// 새로 꽂은 프로브의 값으로 읽힌다 — 프리셋이 다르면 전혀 다른 수다.
|
||||||
|
//
|
||||||
|
// 측정 중이었으면 모드도 내린다. 끊긴 채로 돌면 3초마다 시간 초과만 쌓인다.
|
||||||
|
LaunchedEffect(isConnected) {
|
||||||
|
if (isConnected) return@LaunchedEffect
|
||||||
|
mode = null
|
||||||
|
outcome = null
|
||||||
|
live = emptyList()
|
||||||
|
window = emptyList()
|
||||||
|
tick = 0
|
||||||
|
error = null
|
||||||
|
// 저장 결과 문구는 남긴다 — 방금 남긴 기록이 사라지면 남았는지 의심하게 된다.
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(mode) {
|
LaunchedEffect(mode) {
|
||||||
val m = mode ?: return@LaunchedEffect
|
val m = mode ?: return@LaunchedEffect
|
||||||
lastMode = m
|
lastMode = m
|
||||||
|
|||||||
@@ -142,6 +142,26 @@ fun HospitalModeView(appState: AppState) {
|
|||||||
mutableStateOf<com.medithings.vesiscan.managers.DetachWatcher.Snapshot?>(null)
|
mutableStateOf<com.medithings.vesiscan.managers.DetachWatcher.Snapshot?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 연결이 끊기면 기기에서 온 값을 비운다 ───────────────────────────────
|
||||||
|
// 버튼에만 넣지 않고 isConnected 를 본다. 프로브가 스스로 끊기는 경우가 실제로
|
||||||
|
// 있다(VBTFW0206 freeze — 2026-09-08 로그에서 advertising 까지 멈췄다). 그때도
|
||||||
|
// 화면은 마지막 값을 계속 보여 주므로 구분이 안 된다.
|
||||||
|
//
|
||||||
|
// 왜 비워야 하나: **프리셋이 다른 프로브로 갈아 끼우면 같은 신호가 전혀 다른 용적이
|
||||||
|
// 된다.** PiezoHW.activePreset 이 dps·delay 를 정하고, 실측에서 같은 데이터가
|
||||||
|
// 125mL 와 490mL 로 갈렸다. 끊긴 프로브의 187mL 가 "방광 용적"이라는 이름으로 남아
|
||||||
|
// 있으면 새 프로브의 값으로 읽힌다.
|
||||||
|
//
|
||||||
|
// 비우는 것은 **기기에서 온 값만**이다. 자세·충만도·반복·두께는 조작자가 고른
|
||||||
|
// 프로토콜 설정이고, 환자명과 미업로드 목록은 기기와 무관하다 — 특히
|
||||||
|
// lastRunDir/pendingUploads 를 비우면 아직 못 올린 파일을 다시 올릴 길이 끊긴다.
|
||||||
|
LaunchedEffect(isConnected) {
|
||||||
|
if (isConnected) return@LaunchedEffect
|
||||||
|
runBv = null
|
||||||
|
liveChannels = emptyList()
|
||||||
|
detachWatcher.reset(); detachSnapshot = null
|
||||||
|
}
|
||||||
|
|
||||||
val repeats = repeatsText.toIntOrNull()?.coerceIn(1, 200) ?: HOSPITAL_DEFAULT_REPEATS
|
val repeats = repeatsText.toIntOrNull()?.coerceIn(1, 200) ?: HOSPITAL_DEFAULT_REPEATS
|
||||||
val canStart = isConnected && patient.isNotBlank() && !running
|
val canStart = isConnected && patient.isNotBlank() && !running
|
||||||
|
|
||||||
@@ -375,6 +395,10 @@ fun HospitalModeView(appState: AppState) {
|
|||||||
// 기기가 바뀌면 **정렬도 무효**다. R100/R200/R300 은 빔 각도가
|
// 기기가 바뀌면 **정렬도 무효**다. R100/R200/R300 은 빔 각도가
|
||||||
// 서로 달라(0/-7/-14/-21 vs 0/-9/-18/-27) 같은 자리에서도
|
// 서로 달라(0/-7/-14/-21 vs 0/-9/-18/-27) 같은 자리에서도
|
||||||
// nch·cap_frac 이 달라진다 — 최적 부착 위치 자체가 다르다.
|
// nch·cap_frac 이 달라진다 — 최적 부착 위치 자체가 다르다.
|
||||||
|
//
|
||||||
|
// BV·파형·접촉은 여기서 비우지 않는다. 위의
|
||||||
|
// LaunchedEffect(isConnected) 가 **끊기는 모든 경우**를
|
||||||
|
// 맡는다 — 프로브가 스스로 끊길 때도 같아야 한다.
|
||||||
appState.anchorCm = null
|
appState.anchorCm = null
|
||||||
appState.anchorBasis = null
|
appState.anchorBasis = null
|
||||||
bleManager.disconnect()
|
bleManager.disconnect()
|
||||||
|
|||||||
Reference in New Issue
Block a user