증상: Hint 1 ↔ 일러스트 사이 간격이 (Sensor Alignment 타이틀 ↔ Hint 1) 보다
훨씬 커서 비대칭. 일러스트 바로 위만 공간이 비어 보임.
원인: 일러스트 Box 가 .weight(1f).aspectRatio(...) 조합 →
weight(1f) 가 Column 의 남은 vertical 공간을 모두 잡아먹음.
aspectRatio 로 Box 가 실제로 그려지는 높이는 width × 0.84 이지만,
weight 할당량 - 실제 높이 = 빈 공간이 Box 안쪽에 발생.
수정:
- 일러스트 Box 의 .weight(1f) 제거 — aspectRatio 만으로 자연 크기 결정
- Hint 1 ↔ 일러스트 사이 Spacer 8+2dp → 16dp (top bar 의 내부 vertical padding 과 매칭)
결과:
- 타이틀 ↔ Hint 1: 16dp
- Hint 1 ↔ 일러스트: 16dp (균등)
- 일러스트 하단 ~ 버튼: 자연 stacking (남는 공간은 화면 맨 아래로 모임)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1) 힌트 메세지 박스 자체 숨김 (내용 없을 때)
기존: waitingForStart 시 Hint 2 = AnnotatedString(" ") — 빈 칸 자체는 유지
변경: 빈 텍스트면 Box 자체 렌더 X (Spacer 도 같이 제거).
Hint 1: waitingForStart 일 때만 렌더
Hint 2: !waitingForStart && displayHint.text.isNotBlank() 일 때만 렌더
레이아웃: 빈 칸이 사라지므로 일러스트 위/아래 여백이 자연스럽게 통합.
2) Hint 1/2 텍스트 상하 중앙 정렬
기존: Text 에 .padding(vertical=14dp) 만 — baseline 기준이라 일부 폰트에서
미세 비대칭 가능.
변경: Box(contentAlignment=Center) 로 감싸고 Text 를 그 안에 배치 →
박스 높이 = padding 14dp top + 텍스트 + 14dp bottom 정확히 매칭.
3) PiezoMonitoring → PlacementGuide 진입 시 항상 V1 (clinical 만 V2)
기존: currentSession.mode == ALIGNMENT 만 체크 → 잔여 세션이 있으면
일반 흐름에서도 V2 강제됨.
변경: !appState.placementFromMonitoring 조건도 같이 체크.
enterPlacementFromMonitoring() 가 placementFromMonitoring=true 설정 →
이 경우 항상 V1.
enterAlignmentSession() (clinical 전용) 만 placementFromMonitoring=false +
mode=ALIGNMENT 이라 V2 로 라우팅.
영향:
- PiezoMonitoring → "재정렬" 진입 시 항상 V1 ✓
- Clinical → Sensor Alignment → V2 ✓
- 위저드 흐름 + 잔여 세션 → V1 (위저드는 placementFromMonitoring 변경 안 함)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1) Clinical alignment session 진입 시 자동 V2, 그 외 자동 V1
PlacementGuide LaunchedEffect 에서 currentSession.mode==ALIGNMENT 면 V2,
아니면 V1 로 GreenZoneConstants.alignmentAlgo 강제.
사용자 토글 불필요 — 컨텍스트로 자동 결정.
2) V2 활성 시 step indicator 4-stage (기존 3)
- V1: Step N/3 (VERTICAL / LATERAL / GREEN — placementPhase)
- V2: Step N/4 (INITIAL_ACCUM / VERTICAL_CLIMB / CH3_STABILIZE / LR_BALANCE
— v2Aligner.phase)
3) Hint 2 waitingForStart 시 공백 — pubic bone 중복 안 나오게
Hint 2 가 displayHint 표시하던 것을 waitingForStart 시 AnnotatedString(" ")
로 교체. 칸 높이 유지, 텍스트만 사라짐.
4) Hint 1 "pubic bone" 빨강 ExtraBold 강조
기존 plain Text → AnnotatedString 으로 변경 + buildAnnotatedString 의
"pubic bone" 부분에 SpanStyle(color=Red, fontWeight=ExtraBold) 적용.
5) 화살표 alpha 1.0 ↔ 0.85 → 1.0 ↔ 0.0 (완전 사라짐)
색상 #E64A19 (deep orange) → Color.Red.
tween 220ms 그대로 — 빨강 화살표가 깜빡임으로 명확히 사라짐.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feature/tab-navigation 의 V2 코드를 demo-final 로 가져와 임상 alignment session
에서 V1/V2 둘 다 선택 가능하도록.
복사된 파일 (feature/tab-navigation 그대로):
walldetect/algo/SgSmoothGeneric.kt
walldetect/algo/methodd/MethodDParams.kt
walldetect/algo/methodd/MethodDPreprocessing.kt
walldetect/algo/methodd/MethodDSpan.kt
walldetect/algo/methodd/MethodDWallSelect.kt
walldetect/algo/methodd/MethodDTgc.kt
walldetect/MethodDDetector.kt
walldetect/MethodDRunner.kt
managers/AlignmentAdvisorV2.kt
(RollingAligner + AlignGuide4Stage + AlignmentAdvice — alignment.py 1:1)
확장:
walldetect/algo/Otsu.kt
+ otsu1dWithSeparability(values) — Python otsu_1d 의 (threshold, sep) tuple 호환
method_d span fallback gate 에서 사용
managers/GreenZoneConstants.kt
+ enum AlignmentAlgo { V1, V2 }
+ @Volatile var alignmentAlgo: AlignmentAlgo = V1 (default = legacy)
ui/views/monitoring/PlacementGuideView.kt
+ val v2Aligner = remember { RollingAligner() }
+ V2 분기 (alignmentAlgo == V2 시): raw 6ch → push → state(accum/commit)
→ directionHint/directionIcon/placementScore/LED 설정
→ bleManager.debugLogger.info("ALIGN_V2 phase=... state=... bestSet=...")
+ ⚙ Settings overlay 에 V1 (legacy) / V2 (new) 토글 (보라색)
+ addAlignmentFrame 호출 시 V2 활성이면 v2Aligner.phase.name 사용 (4-stage)
ui/views/clinical/ClinicalHomeView.kt
+ ClinicalSession.alignmentAlgo 를 현재 GreenZoneConstants.alignmentAlgo 값으로 채움
(이전 demo-final 은 "V1" 하드코딩)
→ V2 토글 후 Sensor Alignment 진입 시 alignment_algo="V2" 가 json 에 기록
영향:
- 기존 V1 동작 그대로 (default 가 V1, 토글 안 하면 동일)
- V2 선택 시 demo-final 에서도 4-stage 진행 가능 + json 라벨 V2 명시
- 알고리즘 토글은 dev 모드 (⚙ 버튼) 에서만 가능 — 일반 사용자 노출 X
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
새 흐름:
ClinicalHome → BLE 연결 + labdb 활성 확인
→ [Sensor Alignment] 버튼 (우측, 보라색)
→ PlacementGuide (alignment session 모드, V1 advisor)
→ 매 cycle phase/score/detected/urineLens/hint 누적
→ [Complete Alignment] 버튼
→ measurement.json (data_type='002') + labdb 자동 업로드
→ ClinicalHome 복귀
Live 6ch 측정은 기존대로:
ClinicalHome → [Live 6ch] → ClinicalLive → measurement.json (data_type='001')
models/ClinicalSession.kt:
+ SessionMode { ALIGNMENT, MEASUREMENT } enum
+ ClinicalSession.mode (default = MEASUREMENT, 기존 동작 유지)
+ ClinicalSession.alignmentAlgo ("V1" demo-final 에서)
+ folderName 에 mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사
services/ClinicalSessionStore.kt:
+ MeasurementCycle 에 alignment 필드 6개 (phase/score/detected/urineLens/hint/icon)
+ addAlignmentFrame(piezo, imu, ...V1 advisor state) 새 메서드
+ writeMeasurementJson:
mode==ALIGNMENT → data_type='002' (R&D alignment)
그 외 = '001' (measurement)
각 cycle JSON 에 align_phase / align_score / align_detected /
align_urine_lens / align_hint / align_icon 첨부
AppState.kt:
+ enterAlignmentSession()
ui/views/clinical/ClinicalHomeView.kt:
+ Live 6ch + Sensor Alignment 2-button Row 로 교체
+ Sensor Alignment: BLE + labdb active 두 조건 모두 OK 시만 활성
+ 비활성 시 안내 텍스트
ui/views/monitoring/PlacementGuideView.kt:
+ 매 cycle ClinicalSessionStore.addAlignmentFrame 호출
(clinical alignment 모드 시) — phase=placementPhase.name, score, detected,
urineLens, hint, icon
+ Start Scanning 버튼이 clinical alignment 모드에선 "Complete Alignment" 로 전환
→ endMeasurement() → CLINICAL_HOME 복귀 (자동 labdb upload)
→ enabled 조건도 항상 활성 (사용자 직접 종료)
labdb 업로드: 기존 LabdbUploader 경로 그대로 — meta.data_type 으로 002/001 구분.
영향:
- 기존 Live 6ch flow: 영향 0 (mode=MEASUREMENT default)
- 기존 PlacementGuide UX: 영향 0 (clinical alignment 모드 아닐 때 분기 X)
- 신규: Clinical 내 alignment-only 세션 채집/업로드 가능
feature/tab-navigation 에 V2 advisor 버전이 별도 적용됨 (commit 173ff4d 까지).
demo-final 은 V1 advisor 만 가능 — V2/Method D/RollingAligner 코드 없음.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1) Auto scan interval default 600 → 500ms
PiezoSettings.autoScanIntervalMs default 변경. 사용자가 별도 조정 안 하면
기본 0.5초 간격으로 측정 사이클 진행.
2) BLE canSendMaa throttle 600 → 500ms
응답이 280~420ms 라 500ms = 평균 + 80~220ms 안전 마진. 디바이스에 무리 X.
throttle 도 같이 내려야 autoScanIntervalMs=500 이 실제로 적용됨.
3) PiezoMonitoring 도넛 상단 배터리 → 연결 끊기면 Disconnect 표시
기존: 배터리 바 + xx% (항상 표시, 비-demo)
변경: isConnected=false → Icons.BluetoothDisabled + "Disconnect" (빨강)
isConnected=true → 기존 배터리 바 + %
demo flavor 는 영향 0 (!isDemoMode 가드 그대로).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
이전: waitingForStart 분기로 hint1 또는 hint2 하나만 표시 → 시작 누르면
레이아웃이 위아래 이동.
변경: 두 hint 영역 모두 항상 렌더 → 일러스트/Step 위치 고정.
Hint 1 (일러스트 위): 'Place VesiScan above the pubic bone, then press Start'
(시작 안내, 고정)
Hint 2 (일러스트 아래): displayHint (동적 — 시작 전엔 안내문, 진행 중엔 방향)
waitingForStart 초기 상태에는 hint 1/2 가 비슷한 메시지 보일 수 있음 (의도된
동작 — 레이아웃 안정성 우선).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
레이아웃 순서 변경:
기존: 타이틀 → Step → 일러스트 → Hint
변경: 타이틀 → Hint1 → 일러스트 → Step → Hint2 → 버튼 → Skip
Hint 두 영역 분리:
Hint 1 (일러스트 위) — 시작 전 안내
"Place VesiScan above the pubic bone, then press Start"
waitingForStart=true 일 때만 표시.
Hint 2 (일러스트 아래) — 측정 중 방향 안내
"Slide up slightly ↑", "Centered, optimizing..." 등.
waitingForStart=false 일 때만 표시.
태블릿 기준 글자 크기:
타이틀 32 → 56sp (ExtraBold) — 화면폭 ≈ 60% 차지
Hint1 신규 26sp Bold
Hint2 22 → 28sp Bold
Step 22 → 28sp + 3-dot indicator 12/8 → 16/10dp
SWEEP/IMU 배지 10 → 13/12sp
Skip 15 → 20sp
기존 버튼 ("Start Alignment" 28sp, "Start Scanning" 28sp) 그대로 유지.
영향:
- 일반 사용자 (demo flavor): 태블릿 시연 화면 정렬 강화
- dev/feature 브랜치: 동일 패턴 cherry-pick 가능 (별개 commit)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
기존:
title "펌웨어 응답 없음"
subtitle "연결됐으나 mid? 응답 미수신. 펌웨어 호환 문제 가능 — 측정 안 될 수 있음."
변경:
title "기기 응답 없음"
subtitle 없음 (timedOut 케이스)
이유:
사내 테스트 피드백 — 일반 사용자에게 mid?/펌웨어 호환 같은 용어는 의미 없고
오히려 불안만 가중. 빨강 배너로 시각 신호는 유지하되 본문은 한 줄로.
outdated(주황) 케이스는 그대로 — 펌웨어 버전 안내가 actionable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
자산:
- drawable/arrow_transparent.png 신규 (132×112 RGBA, 우향 thin arrow)
코드:
- drawLine 3개로 구성하던 화살표 → drawImage(arrowBitmap) 한 번으로 교체
- 4방향은 rotate(degrees, pivot) 로 처리:
right=0° / down=90° / left=180° / up=-90°
- ColorFilter.tint(#E64A19 × arrowAlpha) — 일러스트 톤 매칭 + blink 유지
- arrowDrawW=90f, 높이는 icon aspect(132:112)로 비례
- center 위치는 bladder oval edge + sensorHalfH + gap + arrowSize/2
기존 blink 로직(scanCount % 2 → animateFloatAsState 180ms)은 그대로 동작 —
tint color 의 alpha 에 arrowAlpha 를 곱하므로 PNG 도 매 refresh 깜빡임.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
자산:
- drawable-xxhdpi/body_silhouette.png 신규 (finalstomachillust.png, 2240×1888 ≈ 7:6)
feature/tab-navigation 의 일러스트와 동일 — bezier 토르소를 PNG로 교체
UI:
- Top bar: Box overlay 구조로 ← + "Sensor Alignment" 화면 절대 가운데 (26sp ExtraBold)
- 본문 큰 타이틀(36sp) 제거 — top bar 와 중복
- Body 영역: bezier silhouette → Image(painter=body_silhouette) + 얇은 Canvas overlay
Canvas 는 oval(방광) + sensor PNG + 방향 화살표만 담당
- 비율 .aspectRatio(2240f / 1888f) 고정 → 화면비 무관 portrait 유지
화살표 refresh blink (사용자 요청):
- scanCount % 2 토글 → animateFloatAsState(1f ↔ 0.35f, tween 180ms)
- 매 cycle 마다 화살표 alpha 가 깜빡임 → "업데이트 됐는지" 시각화
- 색감 #FF5722 → #E64A19 (deep orange 700, 일러스트 톤 매칭)
- StrokeCap.Round + 굵기 9→10 (가독성)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1) 배터리 잔량 경고 (BatteryWarningBanner 신규)
임계: 15% 이하 주황 / 5% 이하 빨강 / 미연결 또는 0% 숨김.
PlacementGuide + PiezoMonitoring 상단(FirmwareWarningBanner 옆) 노출.
2) Catheter Threshold → Voiding Reminder 리네임 + ml 자동환산
"방광이 이 정도 차오르면 알림" 보조설명.
숫자 표시: "80%" 큰 글씨 + "≈ 400 mL (of 500 mL)" 보조.
슬라이더/스테퍼는 그대로 (level 1~10 모델 유지, 10% 단위).
3) Settings overlay tap-outside-to-dismiss
- 뒤 dim Box 에 clickable { showSettings = false } — 밖 탭으로 닫힘
- Card 자체에도 no-op clickable — 카드 빈 영역 탭이 뒤 dim 으로
전파되어 의도치 않게 닫히는 거 방지
- 두 modifier 모두 indication = null → 리플 없음 (iOS modal scrim 동작)
덤으로 배경(액션 버튼 등) 으로 터치 새서 voiding 잘못 기록되던
현상도 해결.
기존엔 Unpair (완전 해제) 만 있었음. 데모 중 다른 디바이스로
빠르게 스위치하려면 bond 유지 채 소프트 끊기가 필요 — 일반
설정 탭의 Disconnect / Delete Pairing 분리와 동일 정책.
레이아웃: 우측 Column 으로 위에 Disconnect(회색), 아래
Unpair(빨강) 세로 배치. 32dp 슬림 버튼.
- Disconnect: bleManager.disconnect() + isDeviceConnected=false
(bond/known list 유지, PIN 없이 빠른 재연결)
- Unpair: 기존 확인 다이얼로그 → disconnectAndUnbond
기존 App / Firmware / Serial 정보 섹션에 "Device" 행 추가.
연결된 BLE 디바이스명 (예: VBT26050202) 노출.
connectedDeviceName 이 비어있으면 행 자체 안 그림 → 미연결
상태에선 이전과 동일하게 깔끔.
(feature/tab-navigation 의 설정 → 앱 정보에도 동일 표시 추가)
증상: 디바이스 A → B 로 스위치 시 firmware/battery/serial 이
이전 디바이스 값으로 잠깐 그대로 노출됨. STATE_DISCONNECTED
콜백이 안 떴거나 다른 디바이스로 즉시 스위치하는 경우.
원인: BleManager 가 singleton 이라 state 가 연결-연결 사이에
유지됨. 새 mid?/msn? 응답 받기 전까진 이전 디바이스 값이
보임 → 사용자가 잘못된 정보로 진단.
수정:
- connect(BleDevice) 진입부에 firmwareVersion / hardwareVersion
/ serialNumber / batteryLevel / connectedDeviceName /
isServiceReady 모두 reset
- reconnectToLastDevice 도 동일 처리
- connectByAddress 는 connect 위임 → 자동 커버
연결 시작 ~ mid?/msn? 응답 도착 사이 잠깐 빈 값/0으로 보임 →
정확. 응답 도착하면 즉시 새 값 반영.
증상: dev bypass(Enter Measurement Mode)로 BLE 없이 측정 진입
했는데도 "펌웨어 업데이트 필요" 배너 노출.
원인: BleManager 가 singleton 이라 이전 세션에서 채워진
firmwareVersion 값이 그대로 남아있음. isFirmwareOutdated 가
isConnected 무관하게 그 값만 보고 판단 → false positive.
수정:
- FirmwareWarningBanner 진입부에 isConnected 체크 추가
- 미연결이면 outdated / timedOut 계산 전에 early return
- demo mode / dev bypass / 이전 세션 잔여 값 모두 차단
증상: demo APK 에서 한 번 BLE 연결 후 disconnect → autoReconnect
가 8초 간격으로 영원히 재시도 → isReconnecting=true 영구 유지
→ Clinical Home 진입 시 "BLE Connecting..." 무한 로딩.
원인: scheduleAutoReconnect 가 SCAN_TIMEOUT 마다 자기 자신을
재호출. 종료 조건은 (1) 실제 연결 (2) Bluetooth disabled 뿐.
수정:
- MAX_RECONNECT_ATTEMPTS = 10 상수 (≈ 100~120초 동안 시도)
- attemptAutoReconnect 진입 시 카운트 체크 → 초과 시
isReconnecting=false, reconnectAttempt 초기화, 콜백 invoke
- 사용자가 ClinicalHome 의 [Connect] 버튼 또는 DeviceScan 재진입
으로 명시적 재시도 가능.
토글 누르면 회색 바 자체가 사라지는 것처럼 보이던 버그.
실제로는 클립된 거였음:
showDevPanels=false → useScrollLayout=false → 비스크롤 Column
→ 도넛+Bladdy+액션버튼이 화면 거의 다 차지 → 하단 토글 클립
수정:
- enableScroll = showDevPanels || isDevMode (스크롤 게이트만 분리)
- useScrollLayout 은 그대로 — 도넛 inner circle / 스페이싱 등
시각적 변화는 panel 펼침 여부 기준 유지
- dev 모드면 panel 접힘 상관없이 스크롤 ON → 토글 항상 reachable
명세서 (BLE Command Table) 재확인:
rid: = [tag 4B] [HW 12B] [Serial 12B] [FW 12B] [crc 2B] = 42B
각 12B 필드는 ASCII 가변 + NULL/공백/제어문자 padding
기존 파서는 whitespace split 후 prefix 매칭. padding 이 NULL
이면 동작하지만 다른 바이트(예: 0xFF, 0x20 외 임의값)면 전체가
한 토큰으로 합쳐져 fw 추출 실패. 새 펌웨어에서 firmware 안
뜨던 증상의 원인.
수정:
- field(offset) 헬퍼로 정확히 12B 씩 슬라이스 후 NULL/제어 제거
- 명세대로 4 / 16 / 28 offset 에서 HW / SN / FW 분리
추가 fallback:
- sendFirmwareVersionQuery() 신규 — mfv? 단독 쿼리
- rfv: 응답 핸들러 추가
- service ready 4초 후 firmwareVersion 여전히 비어있으면
mfv? 자동 송신 (mid? 미지원 펌웨어 대응)
다중 기기 페어링 저장은 Clinical 흐름 전용.
일반 사용자는 매 세션 fresh scan + pair — 저장 기능 자체 미노출.
- BleManager.connect 성공 시: ClinicalSessionStore.inClinicalFlow
체크 → false 면 KnownDeviceStore.add() skip
- DeviceScanView: 같은 조건으로 Saved Devices 섹션 노출 X.
knownDevices = emptyList() → 기존 단일 디바이스 리스트 UX
+ FirmwareWarningBanner 케이스 추가:
연결 후 8초간 mid? 응답 미수신 → 빨강 경고
"펌웨어 응답 없음 — 펌웨어 호환 문제 가능"
사용자가 정확한 원인 파악 가능 (구버전 != 응답 자체 안 옴).
새 펌웨어(VBTFW0118+) 와 코드의 BLE throttle/MTU 가정이 맞도록
정렬됨. 더 옛 펌웨어가 연결된 경우 측정이 안 됨에도 사용자
입장에선 원인을 알 수 없어 endless loading 으로 보임 — 그
혼동을 막기 위한 명시적 경고.
- BleManager.MIN_FIRMWARE_BUILD = 118 상수
- BleManager.parseFirmwareBuild("VBTFW0116") → 116
- BleManager.isFirmwareOutdated: 파싱한 빌드 < 118 일 때 true
- 신규 FirmwareWarningBanner 컴포저블 (주황 경고색)
- PlacementGuide / PiezoMonitoring 상단에 노출
펌웨어 미수신 상태(아직 mid? 응답 전)는 false 로 취급 — false
positive 방지.
다중 기기 페어링 작업과 짝 — known 기기로 빠른 스위치 시
완전 해제(Delete Pairing) 말고 소프트 끊기(Disconnect) 옵션
필요. demo-final 에는 통합 설정 탭이 없으므로 기존 overlay 의
Delete Pairing 위에 Disconnect 버튼을 같이 배치.
- Disconnect (회색): bond 유지, 다음 측정 빠른 재연결
- Delete Pairing (빨강): 기존 확인 다이얼로그 그대로
버전: 1.1.0-design (vc25) → 1.1.1-design (vc27)
vc26은 master 의 v1.2.0 이라 충돌 피해 27 로 jump.
상단: KnownDeviceStore.getAll() 의 저장된 기기 카드 리스트
· 탭 = connectByAddress 로 PIN 없이 빠른 재연결
· 휴지통 아이콘 = 페어링 해제 확인 다이얼로그 후 unbond
· 마지막 연결 시각 표시
· 파란 calm 톤 카드 (MlPrimary.copy alpha 0.06)
하단: 스캔된 기기 중 known 에 없는 것만 (New Devices)
· 기존 DeviceRow 로직 그대로
비어있는 상태 / 권한 / Bluetooth off 경고는 기존대로.
기존 단일 last_device_address/name 키를 JSON 리스트로 확장.
여러 각도 변형 기기를 등록해두고 PIN 재입력 없이 골라 연결 가능.
KnownDeviceStore (services/):
- getAll / add / remove / mostRecent
- 구 단일 키 자동 마이그레이션 (read 시 흡수, 새 add 시 호환 유지)
BleManager:
- 연결 성공 시 KnownDeviceStore.add 호출 (단일 키 직접 쓰기 제거)
- autoConnectLastDevice: mostRecent() 사용
- disconnectAndUnbond: known list 에서도 제거
- connectByAddress(addr,name) 신규 — 스캔 결과 없어도 OS bond 가
있으면 바로 연결. KnownDeviceStore 에서 골라 호출.
- unbondAndRemoveAddress(addr) 신규 — 연결 안 된 기기도 페어링
해제 가능 (removeBond + KnownDeviceStore.remove)
회의 추가 결정: Phantom/Human/ETC 카테고리 안에서도 표본을 구분하기 위한
자유 텍스트 식별자가 필요. 예: "500mL phantom", "S001", "small phantom v2".
ClinicalSession 모델:
- val subject: String = "" (optional, 기본값 빈 문자열)
- folderName: prefix로 포함 (비어있으면 생략)
예: PHANTOM-500mL_VBT26050202_SUPINE_ALIGN_0CM_2026-06-09_15-30-00
예 (subject 없음): PHANTOM_VBT26050202_SUPINE_ALIGN_0CM_2026-06-09_15-30-00
- 폴더명 안전 문자만 (특수문자 → '-'), testId 형식과 일관성 유지
ClinicalSessionStore:
- lastSubject: String 추가 (다음 측정 자동 prefill)
- startMeasurement에서 저장
JSON 저장:
- meta.json + measurement.json의 meta 블록에 "subject" 필드 추가
- 비어있으면 필드 자체 생략 (기존 호환)
labdb payload:
- params.subject 필드 추가 (subject_type과 함께)
UI (ClinicalHomeView):
- SubjectType segmented 아래에 OutlinedTextField
- 라벨: 'Subject (선택) — 예: 500mL, S001'
- IME action Done + onDone hide
- 마지막 입력 유지 (lastSubject prefill)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 #8 페이지 통합
- 별도 화면(AUTO_SCAN_SETUP) 폐기 — Personalization 한 화면에 3카드:
1) Max Capacity, 2) Catheter Threshold, 3) Auto Scan Interval
- 캐릭터 작게 (220→120dp) + 카드 영역 verticalScroll
- 'Scan Interval' → 'Auto Scan Interval' 이름 복귀
- 한글 설명 제거
- 흐름: PiezoPersonalization → PlacementGuide (AUTO_SCAN_SETUP 단계 없음)
회의 #9 새 형식 — 모든 위치에서 통일
- 범위 0.5 ~ 10 (sec/min 모드 동일)
- 0.5 단위 (이전 200ms/30s 단위)
- sec/min 토글 숫자 우측에 위치
- SEC 모드: ms = value × 1000 (예: 1.5sec = 1500ms)
- MIN 모드: ms = value × 60000 (예: 1.5min = 90000ms)
- 모드 전환 시 같은 숫자 값 유지 (1.5sec ↔ 1.5min)
회의 #10 in-monitoring settings 동일 적용
PiezoMonitoringView 설정 페이지의 Auto Scan Interval 슬라이더도 새 형식으로 갱신.
같은 로직 (unitMode + valueClamped + sec/min 토글 + 0.5 step).
정리:
- AppState.AUTO_SCAN_SETUP enum value 제거
- autoScanSetupComplete() 함수 제거 (호출처 없음)
- backFromPlacementGuide() → PIEZO_PERSONALIZATION으로 (AUTO_SCAN_SETUP 단계 폐기)
- MainActivity 라우팅/Back handler에서 AUTO_SCAN_SETUP 분기 제거
- AutoScanIntervalSetupView.kt 파일 삭제
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#2 Sensor Alignment 토르소 크기 dev/일반 통일
이전: dev이면 Modifier.height(400.dp) 고정, 일반은 weight(1f) → dev에서 갑자기 작아짐
수정: 둘 다 weight(1f)로 통일 → 가용 공간 균등 분배.
dev 모드의 panel 공간은 위쪽 Hint/Step 영역에서 흡수.
#3 도넛차트 dev mode에서 로그/패널이 안 보이는 문제
원인: showDevPanels default false라서 dev 모드 진입해도 토글 안 누르면 안 보임.
수정: remember(appState.isDevMode) { mutableStateOf(appState.isDevMode) }
→ dev mode ON: 자동 펼침
→ dev mode OFF: 자동 접힘
→ 토글 button은 그대로 사용 가능 (수동 접기/펼치기)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
서버 측 변경(2026-06-09 통지): export 파일명을 session_name 기반으로 생성.
이름에 날짜 있으면 그대로, 없으면 측정시각 추가. testId는 40자 제한이라
잘려도 OK — 파일명은 sessionName 우선.
이전 동작: sessionName = meta.label (예: VBT26050301_SUPINE_ALIGN_0CM)
→ 서버가 측정시각을 append → 짧은 라벨_시각.json 형태
→ 앱 측 measurement_<folder>.json과 불일치
새 동작: sessionName = folderName (풀네임, 날짜+시간 포함)
예: PHANTOM_VBT26050202_SUPINE_ALIGN_0CM_2026-06-09_15-30-00
→ 서버는 그대로 사용 (날짜 중복 없음)
→ 앱 폴더명/파일명과 정확히 매칭
testId는 sanitizeTestId로 40자 한도 내 truncate된 채로 그대로 — 의도된 동작.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 피드백: 분석 PC로 파일만 옮기면 어떤 측정인지 식별 불가.
폴더명에는 SUBJECT_TYPE/device/posture/step/datetime이 다 있는데
파일은 generic 이름이라 폴더에서 빼면 어디 측정인지 모름.
수정:
- meta.json → meta_{folderName}.json
- measurement.json → measurement_{folderName}.json
예: measurement_PHANTOM_VBT26050202_SUPINE_ALIGN_0CM_2026-06-09_15-30-00.json
ClinicalSessionStore에 metaJsonFile / measurementJsonFile 헬퍼 추가.
writeMeta / writeMeasurementJson 둘 다 헬퍼 사용.
LabdbUploader.uploadBlocking 파일 탐색 순서:
1. measurement_{folderName}.json (신규)
2. measurement.json (구 버전 호환)
3. measurement_*.json 패턴 매칭 (폴더명 다를 때 fallback)
→ 셋 다 못 찾으면 IllegalStateException
기존 폴더(구 파일명)는 그대로 업로드 가능. 새 폴더부터 새 형식 적용.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 결정 2건:
1. 6채널 그래프 layout: 2×3 grid → 1열 6행 세로 일렬
- 각 그래프가 화면 width 가득 (가독성 ↑)
- 부모 Column이 이미 verticalScroll을 가지므로 자체 scroll 미적용 (nested scroll 회피)
- SixChannelGrid 내부 Row 분기 제거 → 단순 Column에 ChannelChart × 6 + 4dp spacing
2. Capture 기능 제거 (필요 없음)
- autoCapture = true 기본 + 매 사이클 자동 저장이라 Capture 버튼은 사실상 무용
- 하단 'Capture' 버튼 제거 (이전엔 Pause/Resume 옆에 위치)
- 'Auto Capture 매 사이클 자동 저장' Switch 토글 제거
- var autoCapture 상태 → val autoCapture = true 고정
- 콜백 자동 저장 로직(imuCollector.onComplete 안)은 그대로 — 토글이 없으니 항상 true 경로
- 상단 doc 코멘트도 'Pause/Resume / Capture / End' → 'Pause/Resume / Discard / End'로 갱신
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: 도넛차트(PiezoMonitoring) Settings → Delete pairing 후 HOME 진입 시
'● Connected — Start to resume' chip이 여전히 표시.
원인: 어제 회의 결정으로 goHome()이 BLE 연결 유지(disconnect+isDeviceConnected
reset 제거)로 바뀜. 일반 Home 진입은 이게 맞지만, 명시적 Unpair는 진짜로 끊은
거라 isDeviceConnected를 false로 동기화해야 함.
ClinicalHome Unpair는 이전 fix에서 이미 처리됨. PiezoMonitoring은 누락.
수정: bleManager.disconnectAndUnbond() 직후 appState.isDeviceConnected=false
명시적 처리 후 goHome() 호출.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
스크린샷 피드백: 도넛이 화면을 거의 채우고 컨트롤 버튼이 자연스럽게
최하단으로 떨어지는 느낌이 목표.
이전 변경(420→600)은 한 단계로 부족 — 태블릿 가용 높이(1500dp+)에서
여전히 cap에 묶여 여백이 보임.
수정:
- nonDonutBudget: 280dp → 360dp
실측한 비-도넛 컨텐츠(top bar + status badge + info cards + control
buttons + Dev Tools toggle + spacings)에 더 가까운 값. 도넛 사이즈
계산이 정확해져 spacing 어색함 없음.
- max cap: 600dp → 820dp
큰 태블릿(1500dp+ 가용 높이)에서 도넛이 화면 위쪽을 거의 가득 채움.
- min: 160dp → 180dp (살짝 상향)
폰 영향: availableH-360이 어차피 cap보다 작은 값이라 자동 결정 → 변동 없음.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 스크린샷 피드백: 태블릿에서 도넛 아래 약 35% 빈 여백.
원인: donutFitSize = (availableH - 280).coerceIn(160, 420)
태블릿 가용 높이(1000dp+)가 충분한데 max=420에서 cap에 걸려 묶임.
수정: max cap 420dp → 600dp
- 태블릿: 도넛이 자연스럽게 더 커져 화면 채움
- 폰: availableH-280이 어차피 cap보다 작아 영향 없음
- min 160dp는 유지 (작은 화면 fallback)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 결정 2건:
1. Step/Posture 유지
증상: 2cm 측정 후 돌아오면 Step이 0cm로 자동 reset되어 매번 다시 선택해야 함.
원인: ClinicalHomeView가 매번 ALIGN_0CM/SUPINE으로 초기화.
수정: ClinicalSessionStore.lastPosture / lastStep 추가, startMeasurement 시 저장.
ClinicalHomeView 초기값을 store의 last 값으로.
다른 입력값(examiner, notes, subject_type 등)과 동일한 패턴.
2. BV 단계 제거
회의 결정: Step에서 BV는 불필요.
ClinicalStep enum에서 BV 제거 — 5단계(ALIGN_0CM ~ ALIGN_4CM)만 유지.
다른 직접 참조 없어 안전 (LabeledSegmented가 enum.values()로 자동 반영).
step.isAlignment 필드는 유지 (JSON meta 호환) — 이제 모든 step이 true.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 결정: 잘못 측정한 경우를 없앨 방법이 없었음. 두 시점 모두 제공.
ClinicalSessionStore:
- discardAndDeleteFolder(): 측정 중 즉시 폐기. endMeasurement 호출 안 하고
currentSession + cyclesBuffer 비우고 폴더 전체 삭제.
- deleteFinalizedFolder(): lastFinalizedFolder 로컬 삭제 + reset.
LabdbClient.deleteSession(testId):
DELETE /api/v1/sessions/{testId} — Cascade로 모든 records 함께 제거.
204/200/404 모두 성공 처리 (404 = 이미 없음, 멱등).
401 → 자격 clear + Reregister, 403 → PendingApproval, 429 → RateLimit.
LabdbUploader.deleteSessionForFolder(folder):
labdb_upload.json에 sessionId 있으면 그것을, 없으면 sanitizeTestId(폴더명).
미업로드 폴더에 대해서도 안전하게 호출 가능 (404 처리).
ClinicalLiveView:
- 기존 End Session 단일 버튼 → Discard + End Session 두 버튼 (50/50 Row)
- Discard: 빨간 outline. confirm dialog → 폴더 삭제 + ClinicalHome 복귀
- 사이클 진행 중에도 누를 수 있음 (잘못 측정 즉시 인지 시)
ClinicalHomeView:
- Last saved 카드에 Upload 버튼 옆 (또는 단독) Delete 버튼 추가
- confirm dialog → labdb 서버 DELETE 시도 → 로컬 폴더 삭제 → state reset
- 서버 실패해도 로컬은 진행, Toast로 결과 표시
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 결정: 자유 텍스트 ID 입력 대신 측정 대상을 PHANTOM/HUMAN/ETC 세
카테고리로 명확히 구분. JSON/labdb 저장 방식도 함께 변경.
데이터 모델 (ClinicalSession.kt):
- SubjectType enum 추가: PHANTOM / HUMAN / ETC
- val subjectId: String 제거 → val subjectType: SubjectType
- 폴더명: "{subjectId}_{...}" → "{SUBJECT_TYPE}_{...}"
예: PHANTOM_VBT26050202_SUPINE_ALIGN_0CM_2026-06-08_15-30-00
JSON 변경 (meta.json + measurement.json):
- "subject_id": "S001" → "subject_type": "PHANTOM"
labdb payload (LabdbUploader):
- params.subject_id → params.subject_type (서버는 free JSON이라 코드 변경 불필요)
UI (ClinicalHomeView):
- Subject ID OutlinedTextField 제거
- LabeledSegmented "Subject Type" 3개 버튼 (Phantom/Human/ETC)
- 마지막 선택 기억 (ClinicalSessionStore.lastSubjectType)
ClinicalLive 상단 sub line: "S=S001 · TV=300mL · Ab=22mm"
→ "Phantom · TV=300mL · Ab=22mm"
호환성: 기존 측정 폴더는 그대로. 새 폴더부터 새 형식 적용.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 피드백:
- 도넛차트 화면 비율 이상함 → 화면에 꽉 차게
- ml 숫자 너무 작아서 안 보임 → 키움
- dev mode와 일반 모드 도넛 크기 다름 → 통일
- 아래로 내렸을 때 로그/그래프 확인 가능하게
도넛 sizing 통일:
- nonDonutBudget: dev=280 / 일반=360 → 일관 280dp (도넛에 공간 더 할당)
- donutFitSize: coerceIn(120, 360) → (160, 420) — 화면 꽉 차게 max 키움
- baseDonutSize: dev 분기 제거 → 모두 donutFitSize 사용
- donutLineWidth/bladdySize: dev 분기 제거, 일반 모드 방식으로 통일
- volFontSize: dev=18 / 일반=24 → 일관 40sp (ml 크게)
Logs & Charts 토글:
- 토글 버튼: dev 모드 한정 → 일반 모드에서도 표시
(라벨: dev이면 "Developer Tools", 일반이면 "Logs & Charts")
- useScrollLayout = appState.isDevMode && showDevPanels → showDevPanels만
- verticalScroll: dev 조건 제거 → useScrollLayout 따름
- dev panels 본체: appState.isDevMode && showDevPanels → showDevPanels만
→ 일반 사용자도 토글로 아래 스크롤하여 BLE 로그/채널 그래프 확인 가능.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 피드백: BLE 연결된 상태에서 HOME으로 가면 연결이 끊겨 다음 측정 때
DeviceScan부터 다시 거쳐야 하는 불편. 연결을 유지하고, Start 시 설정 단계
모두 skip하고 바로 도넛차트로 진입할 수 있도록.
AppState.goHome():
- bleManager.disconnect() 제거 — BLE 연결 유지
- isDeviceConnected reset도 제거 (실제 연결 상태 유지)
- Demo 모드인 경우만 isDeviceConnected=false (가짜 연결 정리)
- NIRS 스트리밍 정지 / 데모 종료 처리는 유지
AppState.startFromHome():
- 연결됨(isDeviceConnected==true) → PIEZO_MONITORING 직진 (도넛차트)
- 미연결 → DEVICE_SCAN (기존 흐름)
HomeView:
- 연결된 상태이면 "● Connected — Start to resume" chip 표시
사용자가 다음 Start가 바로 도넛차트로 갈 것임을 인지 가능
기존 동작 유지:
- ClinicalHome Unpair → appState.isDeviceConnected=false (어제 fix 그대로)
→ HOME 진입 시 chip 안 보임 → Start 시 DeviceScan으로 정상 진입
- PiezoMonitoring Disconnect 다이얼로그 → goHome() 그대로 호출되지만
이젠 BLE 끊기지 않음. 의도적으로 끊고 싶으면 disconnectAndUnbond 사용.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회의 피드백:
- 제목 'Auto Scan Interval' → 'Scan Interval'
- 한글 설명 → '측정 간격 조절'
- ms 단위 표시 → sec 기본 + min 토글
- 최소 600ms 고정 (펌웨어 응답 시간 + 안전 마진)
구현:
- SEC 모드: 0.6 ~ 5.0 s (0.2s step) — 짧은 간격, 실시간
- MIN 모드: 1 ~ 10 min (30s step) — 장시간 측정, 배터리 절약
- 토글 버튼(SEC/MIN) 활성 시 진입 모드의 최소값으로 reset
- 슬라이더/스테퍼/표시값 모두 모드에 맞춰 동적 변환
- 기본 모드 추론: 저장된 ms < 60000이면 SEC, 아니면 MIN
PiezoMonitoring in-monitoring settings 슬라이더는 sec-only로 그대로 유지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: Clinical → Unpair → 메인 → Start → 도넛차트 진입 시 BLE 끊긴 상태로
도착. 새 페어링 없이 측정 시도 → 응답 없음.
원인: ClinicalHomeView의 Unpair 다이얼로그가 BleManager.disconnectAndUnbond()
만 호출하고 appState.isDeviceConnected는 안 건드림. BleManager.isConnected.value
는 false로 reset되지만 appState 측은 stale true 상태로 남아 있음.
AppState.startFromHome()이 isDeviceConnected==true 분기에서 DeviceScan을
건너뛰고 곧장 PIEZO_PERSONALIZATION으로 이동하기 때문에, 재페어링 단계
자체가 누락된 채 MaxCapacity → AutoScan → Placement(skip) → 도넛차트까지
미연결 상태로 진행.
수정: Unpair 확인 시 appState.isDeviceConnected = false 한 줄 추가.
이제 Start → DeviceScan 화면으로 가서 정상적으로 재페어링.
참고: PiezoMonitoring의 Disconnect 다이얼로그는 appState.goHome() 호출하고
그 안에서 isDeviceConnected=false 처리되므로 별도 fix 불필요.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
사용자 검증: ble.log에 rsn 응답이 보이지만 도넛차트는 0% 표시.
원인 1 — msn? 송신 포맷:
기존: CRC16.buildCommandBE("msn", intArrayOf(0))
→ bytes: 6D 73 6E 3F 00 00 03 FE (BE param 2B)
→ 펌웨어가 응답 안 함 (rsn: 자체가 안 옴)
다른 query 명령(mid?, mtb?, maa?, mqq?, mag?)은 모두 ASCII " " 포맷.
msn?만 BE param 포맷이라 펌웨어가 인식 못 한 듯.
수정: CRC16.buildCommandASCII("msn", " ")
→ bytes: 6D 73 6E 3F 20 ?? ?? (다른 query와 동일 패턴)
원인 2 — Clinical → Unpair → 새 연결 후 도넛차트 진입 시 batteryLevel=0:
disconnectAndUnbond()에서 batteryLevel.value=0 reset됨.
새 BLE 연결의 onDescriptorWrite → startBatteryPolling 호출은 PiezoMonitoring
진입 전에 이미 완료. PiezoMonitoring의 LaunchedEffect에서 등록하는
onConnectionStateChanged 콜백은 '상태 변경 시점'에만 트리거되므로
이미 connected인 상태로 진입하면 호출 안 됨.
수정: PiezoMonitoring 진입 시 isConnected.value==true이면 명시적으로
startBatteryPolling() 한 번 더 호출 (idempotent — stopBatteryPolling 후 재시작).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
방광 모형 테스트가 100% 통과하는 시점을 demo flavor로 동결하고, 모든
신규 작업은 dev에서만 진행하기 위한 빌드 분기 셋업.
gradle (app/build.gradle.kts):
- flavorDimensions: "channel"
- demo: applicationIdSuffix=".demo", versionNameSuffix="-demo",
BuildConfig.IS_DEMO=true, FLAVOR_LABEL="demo"
- dev: BuildConfig.IS_DEMO=false, FLAVOR_LABEL="dev"
- 두 flavor 동시 설치 가능 (applicationId 분리)
source set:
- src/demo/res/values/strings.xml — app_name "VesiScan Demo"
- src/dev/res/values/strings.xml — app_name "VesiScan Dev"
- src/demo/java/.gitkeep + src/dev/java/.gitkeep — 격리본 둘 위치 안내
운영 가이드: docs/FLAVOR_DEMO_STABLE.md
- Level 1 (BuildConfig 분기) vs Level 2 (source set 격리) 두 동결 방식
- 새 안정판 동결 시 git tag + src/demo/ 복사 절차
- 어떤 코드를 동결 후보로 둘지 (알고리즘/파라미터 권장, BLE/UI는 공유)
- 트러블슈팅 + 첫 동결 시점 권장 절차
.gitignore: /docs/ 제거 — 팀/iOS 개발자가 접근 가능하도록
- docs/BLE_PROTOCOL_REFERENCE.md
- docs/iOS_PORTING_CLINICAL_MEASUREMENT.md
- docs/FLAVOR_DEMO_STABLE.md 모두 신규 commit
검증:
./gradlew assembleDemoDebug + assembleDevDebug 둘 다 BUILD SUCCESSFUL
→ app/build/outputs/apk/demo/debug/app-demo-debug.apk
→ app/build/outputs/apk/dev/debug/app-dev-debug.apk
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
증상: 연결 직후 가끔 배터리 값이 0%로 남아 표시 안 됨 / 의미 없는 값.
원인: startBatteryPolling()이 이름과 달리 연결 시 1회 msn? 송신만 함.
그 한 번이 priority HIGH 협상 직전 큐로 들어가거나, 직후 mtb?/maa? 측정
루프와 큐 경합으로 손실되면 영원히 batteryLevel.value = 0 으로 남음.
이후 측정 응답에 배터리가 안 들어있어 갱신 기회 없음.
수정: 세 단계 보장 구조로 변경
1. 즉시 1회 송신 (기존 동작 유지)
2. 3s 뒤 batteryLevel == 0 이면 최대 2회 재송신 (첫 응답 누락 보강)
3. 30s 주기 정상 polling 추가 (충전 반영 + 장기 추적)
stopBatteryPolling()도 retry/poll 양쪽 타이머 정리하도록 확장.
끊김 시 isConnected.value == false이면 콜백에서 조기 return하여
좀비 송신 방지.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
사내 피드백: PiezoPersonalization 한 화면에 카드 3개를 모은 게 아니라
MaxCapacity 다음 단계로 Auto Scan Interval 화면을 독립적으로 띄우길 원함.
흐름 변경:
before: DeviceScan → PiezoPersonalization (MaxCapacity + Catheter + AutoScan) → Placement
after: DeviceScan → PiezoPersonalization (MaxCapacity + Catheter) → AutoScanSetup → Placement
변경:
- PiezoPersonalizationView 원복: 2카드 / verticalScroll 제거 / spacing 64dp 중앙 정렬
- AutoScanIntervalSetupView 신규 — Bladdy hero + 큰 카드 + Continue 버튼
- AppScreen.AUTO_SCAN_SETUP enum 추가
- personalizationComplete() → AUTO_SCAN_SETUP으로 이동
- autoScanSetupComplete() 신설 — PlacementGuide로 진행, piezoSettings persist
- backFromPlacementGuide() — setup 흐름이면 AUTO_SCAN_SETUP으로 돌아감
- MainActivity 라우팅 + Back handler 추가 (AUTO_SCAN_SETUP → PiezoPersonalization)
PiezoMonitoring의 in-monitoring settings 슬라이더는 그대로 유지 — 측정 중에도
같은 PiezoSettings.autoScanIntervalMs 값을 조정 가능.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>