Commit Graph

355 Commits

Author SHA1 Message Date
dw.jang a175540291 fix(posture): pitch 분류 invert + PiezoMonitoring 설정에도 기상 알람 토글
증상 1: 디바이스 평평하게 둠 → "Upright" / 세움 → "Lying" (또는 기존 빌드의
        "Sitting") 으로 사용자 직관과 반대.

진단: 사용자 raw 데이터 분석
  - 평평 (AZ dominant, AZ ≈ 1g): pitch ≈ 0° → 코드가 NON_LYING (Upright) 반환
  - 세움 (AX dominant, AX ≈ 1g): pitch ≈ 80~90° → 코드가 LYING 반환
  사용자 직관: 평평=Lying, 세움=Upright → 코드 inversion.

수정 1: ImuPostureClassifier 의 분류 조건 invert
  - 기존: |pitch| > threshold → LYING
  - 신규: |pitch| ≤ threshold → LYING (작은 pitch = AZ 가 중력 = 센서 평평)
  - dev slider hint 도 "|pitch|≤N°→Lying" 으로 동기화

수정 2: PiezoMonitoring 의 dev settings overlay 에도 기상 알람 토글 추가
  - 기존: PiezoPersonalizationView 에만 존재
  - 신규: dev settings panel (Pitch Threshold slider 아래) 에 동일 토글
  - 두 위치 모두 같은 WakeupAlarmPrefs 사용 — sync 됨
  - 사용자가 monitoring 페이지에서 바로 켜고 끌 수 있음

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 17:39:46 +09:00
dw.jang 9a5fa16f94 fix(posture): gyro gate 제거 + UNKNOWN streak 화면 표시
증상: 디바이스를 lying 상태로 둬도 "Upright" 표시가 안 바뀜.

진단 (raw 로그 분석):
  - 디바이스 정지 상태에서도 gyro y 축 ≈ -25 dps 일관 표시
  - 이는 IMU 칩의 **calibration bias** (정지 시 zero offset 안 됨)
  - flowchart 의 gyro gate (9.16 dps) 가 항상 fail → classifier 영구 UNKNOWN
  - currentPosture 가 UNKNOWN 시 업데이트 안 되는 로직 때문에 첫 valid
    값 (NON_LYING, 사용자가 손에 들고 진입한 순간) 으로 stuck
  - 사용자가 눕혀도 분류 자체가 안 되니 "Upright" 그대로

수정:
  1) ImuPostureClassifier: gyro gate 제거
     - accel magnitude gate (0.8-1.2g) 가 큰 모션은 이미 거름
     - gyro magnitude 는 Result/log 에 계속 노출 (디버그용)
     - flowchart 그대로 따르려면 gyro 도 활용해야 하지만, bias 큰
       디바이스에선 무용지물이라 실용성 우선

  2) PiezoMonitoringView: UNKNOWN streak 카운터
     - 3회 연속 UNKNOWN 이면 currentPosture = UNKNOWN 으로 강제 표시
     - silent stuck 상태 (잘못된 자세 계속 보여줌) 방지
     - 잠시 1-2 cycle 동안 motion artifact 로 UNKNOWN 떠도 직전
       자세 유지 (이전 동작 보존)

  3) Log: unknownStreak 도 출력 → 디버그 시 stuck 케이스 가시화

테스트:
  - lying → upright 전환 시 즉시 "Upright" 표시되어야 함
  - 정지 시 "Lying" 또는 "Upright" 안정적 유지
  - 큰 모션 (들어올림 등) 중에는 3 cycle 후 "—" 표시

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 17:25:58 +09:00
dw.jang af469558e6 feat(posture): Phase 2 — 기상 알람 (LYING/NON_LYING + Settings 토글 + 빨간 팝업)
요구사항: 설정 탭에서 알람 on/off → on 상태에서 일어남 감지 시 빨간 팝업.

변경:

1) ImuPostureClassifier 2-state 단순화
   - 기존: SUPINE/SITTING/STANDING (3 state)
   - 신규: LYING / NON_LYING (2 state) + UNKNOWN
   - sitting/standing 구분 불필요 (낙상 예방엔 "누웠나/일어났나" 만 중요)
   - 분류: |pitch| > threshold → LYING, ≤ → NON_LYING

2) GreenZoneConstants.pitchLyingThreshold (default 25°)
   - @Volatile var — runtime 변경 가능
   - dev 모드 설정에서 slider 로 조정 (10~70°, 11 steps)

3) WakeupAlarmPrefs — SharedPreferences 저장
   - isEnabled (default false)
   - lyingDurationSec (default 180 = 3분)

4) WakeupDetector — state machine
   - LYING 진입 시 timestamp 기록
   - LYING → NON_LYING 전이 시 elapsed 측정
   - elapsed ≥ lyingDurationSec 이면 onWakeup 콜백
   - 1회 발동 후 LYING 복귀까지 재무장 안 됨 (스팸 방지)
   - UNKNOWN 은 무시 (motion artifact 흔들림 영향 X)

5) PiezoMonitoringView
   - posture 아이콘 매핑 LYING (Hotel, 파랑) / NON_LYING (AccessibilityNew, 초록) / UNKNOWN
   - imuCollector.onComplete 콜백에서 wakeupDetector.update(posture) 호출
   - showWakeupAlarm Boolean state + 빨간 Dialog (Color 0xFFD32F2F)
     · Warning 아이콘 + "환자가 일어났습니다" 메세지
     · 누운 시간 표시
     · 확인 버튼 (사용자 dismiss 필수, 자동 닫힘 X)
     · dismissOnBackPress/ClickOutside = false (실수 dismiss 방지)
   - Demo 모드: posture LYING 고정 (BLE 없음)

6) PiezoPersonalizationView — WakeupAlarmCard 신규
   - 빨간 톤 카드 + Switch
   - "기상 알람 — 환자가 일어나면 빨간 팝업으로 알림 (낙상 예방)"
   - 토글 즉시 WakeupAlarmPrefs.setEnabled

7) Dev 설정 (PiezoMonitoringView 의 dev panel)
   - "Pitch Threshold" 슬라이더 추가 (10°~70°)
   - GreenZoneConstants.pitchLyingThreshold 직접 조정
   - 실시간 반영 — 다음 cycle 부터 적용

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 17:17:58 +09:00
dw.jang 6a06452d7e feat(debug): Raw BLE capture 토글 버튼 + 로거 추가
용도: IMU FSR / 채널 누락 / packet 구조 진단을 위해 BLE notification 의 raw
      bytes 를 가공 없이 그대로 파일로 저장.

신규 RawBleLogger.kt:
  - startCapture() / stopCapture() / isCapturing
  - 파일: Downloads/VesiScan_RawBLE/yyyy-MM-dd_HHmmss_raw.log
  - 포맷: "HH:mm:ss.SSS dir=RX prefix=rim len=188 hex=72 69 6D 3A ..."
  - 매 notification 마다 한 줄
  - 파일 시작/끝에 헤더/푸터 (start, stop time, packet count)

BleManager.processReceivedData:
  - 진입 직후 RawBleLogger.write(data) 호출
  - capturing 아니면 즉시 return (no overhead)

PiezoMonitoringView 상단 status bar:
  - posture 아이콘 옆에 토글 IconButton
  - 활성: 빨간 RadioButtonChecked, 비활성: 회색 FiberManualRecord
  - 토글 시 Toast 로 파일 경로 / packet count 안내

사용:
  1) PiezoMonitoring 진입 → 빨간 ● 버튼 탭 → "Raw BLE capture started"
  2) BLE 측정 진행 (alignment / live / single scan)
  3) ● 다시 탭 → "N packets saved to YYYY-MM-DD_HHmmss_raw.log"
  4) adb pull /sdcard/Download/VesiScan_RawBLE/ 로 파일 회수
  5) 텍스트 에디터로 hex 분석

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 15:44:32 +09:00
dw.jang eee065a76a debug(imu): rim: raw byte/int16 dump (ImuRaw tag) — FSR/format 진단용
증상:
  - JSON 의 imu_samples 에 gz=0 같은 의심값
  - sqrt(ax²+ay²+az²) 가 1g 아닌 ~0.5g 일관 표시
  - 펌웨어 FSR vs 앱 변환 (/8192) 불일치 가능성

수정: ImuPacketCollector.parseRim 에 raw dump 로깅 추가.
  - 전체 byte hex dump (188B)
  - 첫 sample + 마지막 sample 의 raw int16 값 (division 전)
  - 참고용으로 /8192 시 g 환산값 같이 표시

logcat:
  adb logcat | grep ImuRaw

기대 출력:
  ImuRaw: rim FULL HEX (188B): 72 69 6D 3A 00 0F ...
  ImuRaw: rim sample[0] raw int16: ax=-170 ay=4034 az=735 gx=-8 gy=16 gz=0
          (g if /8192: ax=-0.0208 ay=0.4925 az=0.0898)

진단 판정:
  - raw az ≈ 8192 → ±4g FSR 정상
  - raw az ≈ 4096 → ±8g FSR (앱 변환식과 mismatch, FSR=±8g)
  - raw az ≈ 2048 → ±16g FSR
  - raw gz=0 일관적 → 펌웨어가 GZ 안 보내거나 sensitivity 다름
  - bytes 순서 다르면 endian/offset 문제

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 15:33:03 +09:00
dw.jang a17c6dac69 fix(posture): IMU polling 을 msp → mtb 로 변경 (alignment 명령 우회)
증상: msp 명령이 펌웨어에서 rsp: 응답 안 보내는 듯 — adb logcat 에 rsp/POSTURE
      로그 한 줄도 안 보임. msp 자체가 작동 안 함.

수정: msp 대신 mtb (alignment 모드, rim: 응답 보장) 로 polling.

  PiezoPacketCollector:
    var skipNextComplete: Boolean = false 추가
    raa: complete 시 skipNextComplete 가 true 면 onComplete invoke 건너뛰고
    flag reset. 자동 1회 use.

  PiezoMonitoringView LaunchedEffect:
    - msp(5초) → mtb(10초) 로 전환
    - mtb 전에 piezoCollector.skipNextComplete = true 설정 → user 의
      측정 callback 가 posture poll 때문에 spurious fire 되는 거 차단
    - sendMtb() throttled 면 (canSendMaa fail) flag rollback
    - 측정 중 (isMeasuring/isAutoMeasuring/isSpotInProgress) 일 땐 skip
      → 사용자 측정과 충돌 방지

trade-off: BLE 트래픽 ↑ (10초마다 mtb = piezo 6ch + IMU). msp 보다 무거움.
          하지만 msp 미작동이라 다른 선택지 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 15:04:06 +09:00
dw.jang 33b072c336 debug(posture): logcat 로 IMU 흐름 가시화 — ImuDbg 태그 도입
증상: PiezoMonitoring 진입해도 자세 ?_ 만 표시, logcat 에 rsp/POSTURE
      로그 안 보임.

원인: debugLogger.tx/rx 는 내부 Entry 리스트에만 적재하고 android.util.Log
      호출 안 함 → adb logcat | grep 에 안 보임.

수정: 핵심 IMU 흐름에 android.util.Log.d("ImuDbg", ...) 추가.

  PiezoMonitoringView.LaunchedEffect:
    - 시작 시 isConnected/isDemoMode 값 로그
    - sendImuQuery 매 호출 "→ sendImuQuery (msp)"
    - 연결 안 되어 있으면 "skip (not connected)"
    - imuCollector.onComplete 결과 (자세 + pitch/acc/gyro)
    - imuCollector.onLog 도 ImuDbg 로 forward (parseRsp 로그 보임)

  BleManager:
    - rsp: 응답 시 hex dump 도 ImuDbg 로 (debugLogger 와 별개)
    - rxs: (cmd_not_supported) 응답 시 ImuDbg 로 warn

  rxs: 중복 핸들러 정리 (기존 1개 → 새 1개로 통합)

테스트: "adb logcat | grep ImuDbg" 한 줄로 전체 흐름 확인 가능.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 14:30:50 +09:00
dw.jang 1774681a18 fix(posture): rsp: 응답 (msp 명령 응답) 파싱 추가 — 자세 분류 작동
증상: PiezoMonitoring 진입 시 자세 아이콘이 항상 ?_ (UNKNOWN) 만 뜸.

원인: sendImuQuery() 가 보내는 msp 명령은 rsp: 응답을 받는데, 기존 코드는
      rim: (mtb alignment 응답) 만 imuCollector.parseRim() 으로 처리.
      rsp: 는 onImuReceived callback 으로만 가는데 어디서도 set 안 됨
      (dead callback) → 자세 분류기에 sample 절대 도달 못 함 → UNKNOWN.

수정:
  1) ImuPacketCollector.parseRsp() 신규
     rsp: format spec 미확정이라 defensive parsing 으로 두 변형 시도:
     - 변형 1 (multi-sample): rim: 와 동일 [tag 4B][num 2B][N×12B][crc 2B]
     - 변형 2 (single sample): [tag 4B][12B sample][crc 2B] = 18B
     실패 시 hex dump 로그 출력 (다음 디버깅용)
     성공 시 imuCollector.onComplete 정상 호출 → 분류기 작동.

  2) BleManager rsp: 핸들러
     - parseRsp() 라우팅
     - rx 로그에 hex 추가 (실제 format 확인용)

기존 rim: 처리는 그대로 (alignment 모드 영향 없음).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 14:06:40 +09:00
dw.jang cf476971a5 feat(posture): Phase 1 — flowchart 기반 자세 분류 + 도넛 우상단 아이콘 (demo-final)
allignment flowchart "Posture classification" 알고리즘 적용. Phase 1
범위: 자세 분류 + 화면 표시만 (알람/감지 없음).

1) ImuPostureClassifier.kt 신규
   flowchart 단계 그대로:
   - Acc magnitude 게이트: 0.8 ≤ |a| ≤ 1.2 (motion/bad reject)
   - Gyro magnitude 게이트: |g| ≤ 9.16 dps (회전 reject)
   - Pitch = atan(ax / sqrt(ay² + az²)) × 180/π
   - 분류:
     · |pitch| < 25°  → SUPINE
     · 25-70°         → SITTING
     · > 70°          → STANDING
   - 게이트 실패 시 UNKNOWN + 사유 반환
   - 입력: ImuSample 리스트 (한 응답 15 sample 평균)
   ※ flowchart 의 "|pitch| > 25; supine" 는 typo 로 판단 → < 25 적용

2) PiezoMonitoringView IMU polling 추가
   - 5초 주기로 sendImuQuery() → rim: 응답 → classifier
   - UNKNOWN 결과 시 직전 자세 유지 (artifact 흔들림 방지)
   - demo mode: SUPINE 으로 fix (BLE 없음)

3) 도넛 차트 우상단 자세 아이콘 표시 (Battery 옆)
   - SUPINE   → Icons.Hotel (파랑)
   - SITTING  → Icons.AirlineSeatReclineNormal (주황)
   - STANDING → Icons.AccessibilityNew (초록)
   - UNKNOWN  → Icons.HelpOutline (회색)
   - 아이콘 + 텍스트 라벨 (Lying/Sitting/Standing/—)

Phase 2 (다음): Settings 토글 + wake-up state machine + 빨간 팝업.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 12:24:22 +09:00
dw.jang 1fc1a20bb9 fix(align V2): LR_BALANCE 좌우 화살표 oscillation 차단 (deadband + 연속 악화)
증상: ch3 확인 통과 후 LR_BALANCE 단계에서 좌/우 화살표가 빠른
      간격으로 번갈아 나타남.

원인:
  AlignGuide4Stage.LR_BALANCE 의 hill-climbing 반전 조건이
  imbal > lateralImbal 만 검사 → 1 sample 라도 늘면 즉시 반전.
  - 노이즈/호흡 으로 imbal 가 ±1-2 fluctuate → 잘못된 반전
  - sliding window 평균 (10-frame) 잔여 효과: 방향 바꿔도 직전
    데이터 섞여 imbal 변화 못 따라옴 → 가짜 "악화" 판정
  - 사용자 반응 지연 동안 imbal 더 누적 → 또 반전 → ←→←→ 진동

수정 (A + C 조합):
  1) A. 반전 deadband (lrReverseDeadband = 3 samples ≈ 6 mm)
     imbal 가 직전보다 "deadband 초과" 증가해야 악화로 카운트.
     LAT_TOL(8) 의 약 1/3 — 정렬 완료 조건 위협하지 않으면서 노이즈
     흡수.
  2) C. 연속 N회 악화 요구 (lrReverseConsecutive = 2)
     deadband 초과 증가가 2 commit 연속이어야 방향 반전.
     단발 노이즈/sliding window 잔여 효과 흡수.

  상태 추가: lateralWorseStreak Int (reset 포함).
  로직:
    - 개선 또는 deadband 안 변동 → 같은 방향 유지, streak = 0
    - deadband 초과 증가 → streak++
    - streak >= 2 → 방향 반전, streak = 0
  lateralImbal 은 매 commit 갱신 (기존 동작 유지).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-22 15:36:19 +09:00
dw.jang fea5879cdc fix(align V1): edge fail hint 간소화 + step 3 회귀 차단
1) hint 텍스트에서 채널 정보 제거:
   "Slide up slightly ↑  (ch3 short)" → "Slide up slightly ↑"
   "Slide down slightly ↓  (ch0 short)" → "Slide down slightly ↓"
   사용자 피드백 — 채널 번호 표시 빼고 방향만.

2) edge 만 fail (CV·lrPass·rPeakOk 통과) 인 경우 GREEN 유지:
   기존: 방향 화살표면 무조건 nextPhase = VERTICAL → step indicator 3 → 1 회귀.
   변경: edgeOnlyFail 시 stayGreen=true → nextPhase = GREEN, step 3 유지.
   이유: edge 비대칭은 mm 단위 미세조정 — 전체 vertical 재시작 불필요.
   CV/lrPass 가 진짜 어긋난 경우는 기존대로 VERTICAL 회귀.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 11:16:24 +09:00
dw.jang c3817c9f80 fix(align V1): GREEN 게이트에 ch0/ch3 edge 대칭 검사 추가
증상: 270 mL 방광인데 220 mL 로 측정 (~50 mL 저평가).
      원인 — 센서가 살짝 아래쪽에 부착돼 ch3 가 방광 하단 가장자리
      만 보고 짧게 잡힘. ch0~ch3 CV 는 0.10 임계 안에 떨어져서
      "In position!" 통과되지만 BV 결과는 저평가.

원리: CV 는 4채널 전체 변동성 — outlier 1개를 N=4 로 희석.
      예: depths (75,78,75,65) → mean 73.25, std 4.79, cv 0.065
      → CV 0.10 통과지만 ch3 가 10mm 짧음.

수정: edge 대칭 게이트 (|d_ch0 - d_ch3| / mean(d0, d3) ≤ 0.08)
      추가. CV 와 직교 — 균일 케이스는 영향 없고, edge outlier
      케이스만 잡음.

  - DEFAULT_EDGE_ASYM_THR = 0.08 도입
  - computePlacementGuide GREEN 체크에 edgeAsym/edgePass 계산
  - isPass = cv ≤ cvThr && lrPass && rPeakOk && **edgePass**
  - hint 분기: edgeAsym > 0 → "Slide up slightly ↑ (ch3 short)"
              edgeAsym < 0 → "Slide down slightly ↓ (ch0 short)"
  - Log 에 edgeAsym/edgePass 추가

CV(0.10) 는 기존 유지. edge 만 보강.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-18 11:11:53 +09:00
dw.jang 8c07809d58 fix(align V1): GREEN 게이트 강화 — 4개 center 모두 요구 (측정 BV 게이트와 정렬)
증상: 정렬 OK 직후 측정 시 ch3 미검출로 BV SKIP. 정렬은 통과했는데
      측정에서 "No center channels" 가 자주 발생.

원인: 정렬 GREEN 게이트는 depths.size >= 2 만 요구 (2~3 center 채널
      만으로도 CV/lr 통과 시 OK). 반면 측정 BV 게이트는
      effectiveCenterWalls >= 4 필요. 정렬 OK 직후 ch3 가 borderline
      이면 측정 단계에서 빠질 확률 높음.

수정: PlacementGuideView.computePlacementGuide() 의 GREEN 게이트를
      depths.size < 4 로 강화 (측정 게이트와 동일). 4개 모두
      잡힐 때만 CV/lr 평가로 진입.
      빠진 채널 위치별 hint 분기:
        - 2개 이상 missing → 일반 "adjust position"
        - ch3 missing → 'Slide up slightly ↑' (사센서 너무 낮음)
        - ch0 missing → 'Slide down slightly ↓' (센서 너무 높음)
        - ch1/ch2 missing → generic 미세 조정
      phase 는 GREEN→VERTICAL 로 회귀 (기존 < 2 분기와 동일).

효과: 정렬 OK 가 4-channel 안정 검출 필요 → 그 다음 measurement
      에서 ch3 미검출로 BV SKIP 거의 사라짐.

트레이드오프: 정렬 단계가 다소 까다로워짐 (작은 방광/borderline
              ch3 사용자 좌절 가능). 필요 시 사용자별 relax 조정.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 18:10:25 +09:00
dw.jang ac6013e373 fix: 큰 볼륨 텍스트의 currentLevel fallback 제거 (150 mL leak)
증상: PiezoMonitoring 에서 single scan 후 home → 재진입 시 갑자기
      150 mL (또는 50/100/200… 등 50 mL 배수) 가 큰 텍스트에 등장.

원인: 측정 성공 시 line 442-447 가 BV 값을 currentLevel 로
  양자화해 SharedPreferences 에 persist (level = (bv/500)*10).
  재진입 시 displayMaxVolumeMl 가 remember 라 0 으로 reset 되고,
  큰 텍스트의 두 번째 fallback (appState.estimatedVolume = currentLevel/10 * 500)
  이 발동 → 직전 측정의 quantized 값이 큰 글씨로 leak.
  170 mL 측정 → currentLevel=3 → 재진입 시 150 mL 표시 등.

수정: 큰 볼륨 텍스트의 fallback 분기 (else if estimatedVolume>0)
      삭제. displayMaxVolumeMl 가 set 됐을 때만 표시, 아니면 무조건
      "— mL". level/urgency 시스템 자체는 그대로 유지 (도넛
      차트, InfoCard, 알림 등 다른 곳에서 계속 사용).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 18:04:29 +09:00
dw.jang b92b166b82 fix: Start 직후 Hint 2 에 "Place above pubic bone" 가 잠시 떴던 leak 제거
증상: Start Alignment 버튼 누른 직후 Hint 2 (아래쪽 박스) 에
      "Place VesiScan above the pubic bone, then press Start" 가
      잠깐 표시됨. 이 메세지는 Hint 1 (위쪽 박스) 전용.

원인: Start onClick 핸들러가 waitingForStart=false 만 바꾸고
      directionHint 는 그대로 둠. directionHint 의 초기값/리셋값이
      "Place...above pubic bone" 이라 V1 placement 엔진이 첫
      cycle 에 새 hint 를 쓸 때까지 그 stale 값이 Hint 2 로 leak.
      (waitingForStart=false 가 되면 Hint 2 가시 조건 충족)

수정: Start onClick 에 directionHint="" + directionIcon=""
      추가. displayHint 가 " " (whitespace) → isNotBlank()=false
      → hint2Visible=false → 박스 자체가 사라짐 (배경도 투명).
      엔진이 첫 commit 에서 새 hint 를 쓰면 Hint 2 정상 등장.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:46:54 +09:00
dw.jang a1d68bd007 ux: Sensor Alignment — Step 0/N, arrow↔hint sync, arrow 위치, oval 색
1) 시작 전 (waitingForStart) Step 표기:
   "Step 1/3" → "Step 0/3" 으로 표시, 점/체크포인트 row 전체
   숨김 (시작 후에만 점 등장).

2) 화살표 ↔ Hint 2 텍스트 mismatch 수정 (V1 path):
   debounce 중 else 분기에서 directionIcon 을 guideResult.icon
   으로 즉시 갱신 → 텍스트는 "위로 올려" 인데 화살표는 가끔
   아래를 가리키는 깜빡임 발생.
   수정: directionHint 텍스트에서 방향 추론 (phaseHoldHint 와
   동일 로직) → 화면에 보이는 텍스트와 항상 일관.
   V2 path 는 이미 동일 source(st.action) 라 영향 없음.

3) 화살표 위치 — 센서/방광 쪽으로 당김:
   vertCloser = arrowDrawH * 0.45f 도입.
   - 위 화살표 cya: 기존 + vertCloser  (살짝 아래로)
   - 아래 화살표 cya: 기존 - vertCloser (살짝 위로)
   좌/우 화살표는 변경 없음.

4) 방광 oval 색 (센서 주위 테두리) — 미정렬 상태:
   Cyan → Color(0xFFFFB74D) (Material orange 300, warm hint).
   greenZone 진입 시는 기존 Green 4CAF50 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:25:20 +09:00
dw.jang f2fb561b9a ux: Hint 박스 배경도 텍스트와 함께 사라짐 (자리만 유지)
수정: Hint 1 / Hint 2 의 background 색을 조건부로 전환.
      - 텍스트 가시: 기존 색 (orange/green/teal)
      - 텍스트 비가시: Color.Transparent
      heightIn 으로 박스 공간(높이)은 그대로 유지 →
      배경 카드만 사라지고 자리는 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:58:29 +09:00
dw.jang b461bfffba ux: Sensor Alignment 레이아웃 정형화 — 박스 항상 자리, weight 5등분
요구사항 (사용자 정의 순서):
  Title / spacer / Hint1 / spacer / Illustration / spacer /
  Step / spacer / Hint2 / spacer / Start / Skip

변경:
  1) Hint 1 박스: if(waitingForStart) 외부 가드 제거 → 박스는
     항상 렌더. 텍스트만 waitingForStart 시 표시. heightIn min
     96dp (현재 자연 높이 ~64dp 의 1.5배).
  2) Hint 2 박스: 동일 패턴 — 박스 항상 렌더, 텍스트는
     hint2Visible 시만, heightIn min 102dp (28sp 텍스트 기준 1.5배).
  3) Step ↔ Hint 2 간격: Spacer 8dp → weight 1f (5번째 분할).
  4) Skip ↔ 하단: weight 1f → 고정 16dp (사용자 요구 — Skip 이
     화면 아래 가깝게).

결과: 빈 공간이 5개 위치 (Title↔Hint1, Hint1↔Illustration,
      Illustration↔Step, Step↔Hint2, Hint2↔Button) 에 균등 분배.
      내용 없을 때도 Hint 박스 자리는 그대로 유지 (배경/높이).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:54:11 +09:00
dw.jang deb47645d8 ux: 화살표 깜빡임 박자를 Hint 2 도트 변화에 맞춤 (snap)
증상: Hint 2 의 ". .. ..." 도트는 매 scanCount 마다 instant 변화,
      화살표는 같은 scanCount 에 220ms tween 으로 fade — 같은 tick
      에 시작하지만 fade 가 끌어서 시각적 박자가 어긋나 보임.

수정: animationSpec tween 220 → 0 (snap).
      화살표가 도트와 동일하게 한 시각에 on/off 토글.
      둘 다 scanCount driver 위에서 같은 박자로 움직임.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:34:06 +09:00
dw.jang 271e20f131 ux: Sensor Alignment 빈 공간 5등분 (Title↔Hint1, 일러스트↔Step 추가)
기존 3-spacer 분배에서 2개 더 추가 → 5등분 균등 분배:

  1) Title(top bar) ↔ Hint 1                    ← 신규
  2) Hint 1 ↔ 일러스트
  3) 일러스트 ↔ Step 1/3                         ← 신규 (Spacer 8dp → weight 1f)
  4) Step/Hint 2 ↔ Start Alignment 버튼
  5) Skip ↔ 화면 하단

결과: 화면 위/아래 어느 쪽에도 빈 공간 쏠림 없음.
인접 요소간 fixed gap (Hint 1 위 padding 등) 은 그대로 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:09:21 +09:00
dw.jang c12aada409 ux: Sensor Alignment 화면 빈 공간 균등 분배 (demo-final)
증상: 화면 아래쪽에만 큰 빈 공간 — 사용자 스크린샷 확인.

수정: 3개 위치의 fixed Spacer 를 Spacer(weight=1f) 로 교체 →
      Column 의 남은 vertical 공간을 3등분 균등 분배.

  1) Hint 1 ↔ 일러스트 (Spacer 16dp → weight 1f)
  2) Step/Hint 2 ↔ Start Alignment 버튼 (Spacer 24dp → weight 1f)
  3) Skip 아래 (Spacer 32dp → weight 1f)

결과:
  - 화면 전체에 골고루 분포, 아래쪽 큰 빈 공간 해소
  - 인접 요소간 fixed gap 은 그대로 유지 (illustration ↔ Step 의 8dp 등)

비고: dev mode (verticalScroll) 에서는 weight 가 0 으로 collapse 되어
      자연 stacking. 일반 사용자 모드만 균등 분배 효과 적용.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 16:00:14 +09:00
dw.jang 8ec063189d ux: Sensor Alignment 일러스트 상하 여백 균등 (demo-final)
증상: 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>
2026-06-17 15:52:00 +09:00
dw.jang 3361ed00f2 ux: Sensor Alignment hint 빈 칸 숨김 + V2 게이트 강화 (demo-final)
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>
2026-06-17 15:43:49 +09:00
dw.jang 1a2f239e4d ux: Sensor Alignment 사용자 피드백 5건 (demo-final)
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>
2026-06-17 14:44:30 +09:00
dw.jang 718016af7a feat: V2 alignment (Method D + alignment.py) demo-final 에 이식
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>
2026-06-17 14:03:34 +09:00
dw.jang 429964571f feat: Clinical Mode 에 Sensor Alignment 세션 추가 (demo-final, V1 전용)
새 흐름:
  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>
2026-06-17 13:16:49 +09:00
dw.jang 0d938af352 ux: Hint 1 텍스트만 Start 후 사라지게 (칸 유지)
waitingForStart=false 이면 Hint 1 의 텍스트를 ' ' (공백) 으로 교체.
배경/패딩/크기는 그대로라 칸 높이 변동 X — 레이아웃 안정 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 11:44:08 +09:00
dw.jang 062c611649 feat: auto scan interval 500ms 기본 + 배터리 → Disconnect 전환
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>
2026-06-17 10:08:10 +09:00
dw.jang 7b8b8fb6ae ux: Sensor Alignment hint 1/2 둘 다 상시 표시 (레이아웃 안정)
이전: 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>
2026-06-17 09:52:18 +09:00
dw.jang c1533a3422 ux: Sensor Alignment 태블릿 레이아웃 재배치 + 글자 키움 (demo-final)
레이아웃 순서 변경:
  기존: 타이틀 → 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>
2026-06-17 09:44:21 +09:00
dw.jang 5e39237a36 ux: Sensor Alignment 4종 개선 (demo-final) — feature/tab-navigation 와 동등
1) 배너 거짓 양성 — BLE 데이터 수신 중인데 "기기 응답 없음" 뜨던 문제
   - BleManager.lastBleRxAt 추가, reb 수신마다 갱신
   - FirmwareWarningBanner: 최근 5초 내 RX 있으면 timedOut 무시
   - 1초 tick 으로 자동 재평가

2) Sensor Alignment 타이틀 26 → 32sp ExtraBold

3) 본문 레이아웃 재배치
   기존: Step → Hint → 일러스트
   변경: Step → 일러스트 (위로) → Hint (그림 아래, 22sp)

4) PNG 화살표 더 크게 + 살색 안 비치는 깜빡임
   - arrowDrawW 90 → 130 (44% 키움, 두꺼움 효과)
   - alpha 1.0 ↔ 0.85 (was 1.0 ↔ 0.35) — 살색 비침 차단
   - tween 180 → 220ms

추가:
  - mls 로그 파서 16bit BE 수정 (feature 와 동일 — 이전엔 항상 "mode 0" 표시)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 10:32:55 +09:00
dw.jang e699567897 ux: 기기 응답 없음 배너 단순화 — 기술 용어(mid?/펌웨어 호환) 제거
기존:
  title    "펌웨어 응답 없음"
  subtitle "연결됐으나 mid? 응답 미수신. 펌웨어 호환 문제 가능 — 측정 안 될 수 있음."

변경:
  title    "기기 응답 없음"
  subtitle 없음 (timedOut 케이스)

이유:
  사내 테스트 피드백 — 일반 사용자에게 mid?/펌웨어 호환 같은 용어는 의미 없고
  오히려 불안만 가중. 빨강 배너로 시각 신호는 유지하되 본문은 한 줄로.
  outdated(주황) 케이스는 그대로 — 펌웨어 버전 안내가 actionable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 15:17:08 +09:00
dw.jang c7d4555535 Sensor Alignment: 방향 화살표 → arrow_transparent.png 교체 (rotate + tint)
자산:
  - 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>
2026-06-15 14:19:06 +09:00
dw.jang a6b973b284 Sensor Alignment: PNG illust + top-bar title + 화살표 refresh blink (feature-parity)
자산:
  - 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>
2026-06-15 14:04:45 +09:00
dw.jang e125e2dd64 ux: 사내 테스트 피드백 3건 — 배터리 경고 / Voiding Reminder 리네임 / 설정 overlay scrim
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 잘못 기록되던
   현상도 해결.
2026-06-15 10:44:31 +09:00
dw.jang c8ceb1369b ui: ClinicalHome BLE 카드에 Disconnect + Unpair 두 버튼
기존엔 Unpair (완전 해제) 만 있었음. 데모 중 다른 디바이스로
빠르게 스위치하려면 bond 유지 채 소프트 끊기가 필요 — 일반
설정 탭의 Disconnect / Delete Pairing 분리와 동일 정책.

레이아웃: 우측 Column 으로 위에 Disconnect(회색), 아래
Unpair(빨강) 세로 배치. 32dp 슬림 버튼.

- Disconnect: bleManager.disconnect() + isDeviceConnected=false
  (bond/known list 유지, PIN 없이 빠른 재연결)
- Unpair: 기존 확인 다이얼로그 → disconnectAndUnbond
2026-06-12 11:44:28 +09:00
dw.jang 31dbca44a6 ui: Settings overlay 에 연결된 Device 이름 표시
기존 App / Firmware / Serial 정보 섹션에 "Device" 행 추가.
연결된 BLE 디바이스명 (예: VBT26050202) 노출.

connectedDeviceName 이 비어있으면 행 자체 안 그림 → 미연결
상태에선 이전과 동일하게 깔끔.

(feature/tab-navigation 의 설정 → 앱 정보에도 동일 표시 추가)
2026-06-12 11:35:46 +09:00
dw.jang 2d32949a14 fix: 매 connect 마다 device-specific state 리셋
증상: 디바이스 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으로 보임 →
정확. 응답 도착하면 즉시 새 값 반영.
2026-06-12 11:14:38 +09:00
dw.jang 2f8a21505e fix: 펌웨어 배너 — 미연결 상태에선 일절 표시 X
증상: dev bypass(Enter Measurement Mode)로 BLE 없이 측정 진입
했는데도 "펌웨어 업데이트 필요" 배너 노출.

원인: BleManager 가 singleton 이라 이전 세션에서 채워진
firmwareVersion 값이 그대로 남아있음. isFirmwareOutdated 가
isConnected 무관하게 그 값만 보고 판단 → false positive.

수정:
- FirmwareWarningBanner 진입부에 isConnected 체크 추가
- 미연결이면 outdated / timedOut 계산 전에 early return
- demo mode / dev bypass / 이전 세션 잔여 값 모두 차단
2026-06-12 10:45:24 +09:00
dw.jang 3179aac513 fix: BLE autoReconnect 무한 루프 — max 10회 시도 후 포기
증상: 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 재진입
  으로 명시적 재시도 가능.
2026-06-12 10:18:05 +09:00
dw.jang dd8a8457b7 fix: dev 모드에서 Developer Tools 토글 클립 해결
토글 누르면 회색 바 자체가 사라지는 것처럼 보이던 버그.
실제로는 클립된 거였음:
  showDevPanels=false → useScrollLayout=false → 비스크롤 Column
  → 도넛+Bladdy+액션버튼이 화면 거의 다 차지 → 하단 토글 클립

수정:
- enableScroll = showDevPanels || isDevMode  (스크롤 게이트만 분리)
- useScrollLayout 은 그대로 — 도넛 inner circle / 스페이싱 등
  시각적 변화는 panel 펼침 여부 기준 유지
- dev 모드면 panel 접힘 상관없이 스크롤 ON → 토글 항상 reachable
2026-06-12 09:19:13 +09:00
dw.jang 497b08164e ble: rid: 파서 fixed-width 12B 필드로 + mfv? fallback
명세서 (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? 미지원 펌웨어 대응)
2026-06-11 15:37:14 +09:00
dw.jang f85b5cfbed ble: 일반 사용자는 KnownDeviceStore 저장 0개 + 펌웨어 미수신 경고
다중 기기 페어링 저장은 Clinical 흐름 전용.
일반 사용자는 매 세션 fresh scan + pair — 저장 기능 자체 미노출.

- BleManager.connect 성공 시: ClinicalSessionStore.inClinicalFlow
  체크 → false 면 KnownDeviceStore.add() skip
- DeviceScanView: 같은 조건으로 Saved Devices 섹션 노출 X.
  knownDevices = emptyList() → 기존 단일 디바이스 리스트 UX

+ FirmwareWarningBanner 케이스 추가:
  연결 후 8초간 mid? 응답 미수신 → 빨강 경고
  "펌웨어 응답 없음 — 펌웨어 호환 문제 가능"
  사용자가 정확한 원인 파악 가능 (구버전 != 응답 자체 안 옴).
2026-06-11 15:34:09 +09:00
dw.jang 1492030333 ble: 펌웨어 구버전 경고 배너 (< VBTFW0118)
새 펌웨어(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 방지.
2026-06-11 15:21:04 +09:00
dw.jang 7be3f0b096 demo: PiezoMonitoring Settings overlay Disconnect 추가 + v1.1.1-design
다중 기기 페어링 작업과 짝 — 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.
2026-06-11 15:05:08 +09:00
dw.jang a2fb0f3554 ble: DeviceScanView 2섹션 — Saved Devices + New Devices
상단: KnownDeviceStore.getAll() 의 저장된 기기 카드 리스트
  · 탭 = connectByAddress 로 PIN 없이 빠른 재연결
  · 휴지통 아이콘 = 페어링 해제 확인 다이얼로그 후 unbond
  · 마지막 연결 시각 표시
  · 파란 calm 톤 카드 (MlPrimary.copy alpha 0.06)

하단: 스캔된 기기 중 known 에 없는 것만 (New Devices)
  · 기존 DeviceRow 로직 그대로

비어있는 상태 / 권한 / Bluetooth off 경고는 기존대로.
2026-06-11 15:03:35 +09:00
dw.jang b782db16b3 ble: 다중 기기 페어링 영속화 (KnownDeviceStore)
기존 단일 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)
2026-06-11 15:03:35 +09:00
dw.jang 7fa18ec05a Clinical: SubjectType 아래 Subject(optional 자유 식별자) 필드 추가
회의 추가 결정: 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>
2026-06-09 17:26:37 +09:00
dw.jang 82ba3bbcf6 UI 묶음 E: Auto Scan Interval 통합 + 0.5단위/sec·min 토글 우측 (회의 #8 #9 #10)
회의 #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>
2026-06-09 17:09:24 +09:00
dw.jang b446226c61 UI 묶음 D: ClinicalLive 6채널 그래프 2x3 복귀 + height 320dp (y축 늘림)
회의 #1: 일렬 6행은 y축 짧아서 기울기 안 보임 → 2x3 grid 복귀 + 셀 height 더 키움.

- layout: 1열 6행 → 2열 3행 (Row × 3 with 2 cells each, weight 1)
- spacing: 4dp → 6dp (셀 사이 여유)
- Card height: 240dp → 320dp (y축 길이 확보 → 기울기 가독성)
- 화면 점유: 3행 × 320dp + spacing ≈ 980dp (태블릿 가용 1500dp의 약 2/3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 17:02:49 +09:00