Commit Graph

477 Commits

Author SHA1 Message Date
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
dw.jang 41712a1577 UI 묶음 B+C: Sensor Alignment 토르소 통일 + dev panels 자동 펼침
#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 17:01:30 +09:00
dw.jang bd99cae98c UI 묶음 A: 도넛차트 정리 — 캐릭터/mL/게이지/카드 (회의 #4 #5 #6 #7)
#4 도넛 안 캐릭터/mL 위치
  - bladdySize: donutSize*0.85 → donutSize*0.85*0.75 (75% 축소)
  - volFontSize: 40sp → 50sp (125%)
  - offset 위로: -0.03 → -0.08 (더 위로)
  - 캐릭터-mL Spacer: 4dp → 0dp (간격 축소)
  → 윗공간 비던 부분 흡수, 그룹이 도넛 가운데 컴팩트 정렬

#5 게이지(stroke arc)가 도넛 외곽선 이탈 → 동일 크기 맞춤
  Compose Canvas의 drawCircle/drawArc는 default로 size 경계에 stroke를
  그리는데 strokeWidth의 절반이 밖으로 튀어나옴.
  → 명시적 inner radius 계산:
     outerR = size.minDimension / 2
     innerR = outerR - strokeWidth / 2
     drawCircle/drawArc 모두 innerR로 그림 → stroke가 size 안에 들어옴.

#6 우측 InfoCard 'Session Max' → MaxRatioCard (% + 측정/capacity)
  새 컴포저블 MaxRatioCard:
    위: "{pct}%" (큰 글씨, urgency 색)
    아래: "{measured} / {capacity} mL"
  예: 47% / 235 / 500 mL

#7 좌측 InfoCard 'Current' → 'Current Measurement'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 16:58:39 +09:00
dw.jang 249b5f1aaa Labdb: payload sessionName을 풀 폴더명으로 — 서버 export 파일명과 일치
서버 측 변경(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>
2026-06-09 16:43:29 +09:00
dw.jang 9f6cb36f00 Clinical: meta.json/measurement.json 파일명에 폴더 정보 포함 (식별성)
회의 피드백: 분석 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>
2026-06-09 16:32:19 +09:00
dw.jang fd9078f9a1 ClinicalLive: 6채널 1열 6행 세로 일렬 + Capture/Auto Capture 토글 제거
회의 결정 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>
2026-06-09 14:45:12 +09:00
dw.jang 4c7d347851 PiezoMonitoring Delete pairing: appState.isDeviceConnected도 false로 동기화
증상: 도넛차트(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>
2026-06-09 14:37:05 +09:00
dw.jang 5703355fc4 Clinical UI: BLE Connecting 상태 + Notes IME Done + 6채널 그래프 키움
3건 동시 적용:

1. ClinicalHome 'BLE Connecting…' 상태 표시
   증상: Unpair 후 새 기기 페어링 시 처음 3~5초간 'BLE Not Connected' 표시.
   원인: connectGatt 호출부터 STATE_CONNECTED까지 시간 걸리는데 그 사이 isConnected==false.
   기존 isReconnecting state는 reconnect 경로 전용이라 새 페어링엔 안 잡힘.
   수정:
   - BleManager.isConnecting state 신규 추가 (mutableStateOf)
   - connectGatt 호출 직전 true, STATE_CONNECTED/DISCONNECTED 시 false
   - 예외 catch에서도 false (실패 시 stuck 방지)
   - ClinicalHome에서 isConnected==false && (isConnecting || isReconnecting)일 때
     CircularProgressIndicator + 'BLE Connecting…' 텍스트 표시

2. ClinicalHome Notes 필드 IME Done
   기존: 여러 줄 입력 가능했지만 키보드 ✓ 버튼이 없어 닫기 어려움.
   수정: singleLine=true + imeAction=Done + onDone=hide keyboard
   Examiner 필드와 동일한 동작.

3. ClinicalLive 6채널 그래프 키움 (2x3 grid 유지)
   회의 결정: layout 그대로, 셀 크기/폰트/선 굵기 모두 키움.
   - Card height: 120dp → 240dp (2배)
   - Card padding: 4dp → 8dp / corner: 6 → 8
   - 색 box: 8dp → 12dp
   - CH label: 10sp → 16sp
   - ant/post label: 9sp → 14sp
   - peak label: 9sp → 14sp + SemiBold
   - waveform stroke: 1.5 → 2.5
   - ant/post 마커 stroke: 1.5 → 2.5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 14:33:37 +09:00
dw.jang d2e7640292 UI: 도넛차트 태블릿 사이즈 더 크게 — cap 600→820, budget 280→360
스크린샷 피드백: 도넛이 화면을 거의 채우고 컨트롤 버튼이 자연스럽게
최하단으로 떨어지는 느낌이 목표.

이전 변경(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>
2026-06-09 11:46:49 +09:00
dw.jang 3095e8b444 UI: 도넛차트 max cap 420→600 — 태블릿 빈 여백 흡수
회의 스크린샷 피드백: 태블릿에서 도넛 아래 약 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>
2026-06-09 11:39:17 +09:00
dw.jang 35f9afbfdb Clinical: Step/Posture 마지막 선택 유지 + BV 단계 제거
회의 결정 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>
2026-06-09 10:47:20 +09:00
dw.jang 1574d659cd Clinical: 잘못 측정한 값 폐기 — Discard(측정 중) + Delete(측정 후 + labdb 서버)
회의 결정: 잘못 측정한 경우를 없앨 방법이 없었음. 두 시점 모두 제공.

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>
2026-06-09 09:51:21 +09:00
dw.jang f3cca9a4c7 Clinical #1: Subject ID 자유입력 → Phantom/Human/ETC 카테고리 (회의 결정)
회의 결정: 자유 텍스트 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>
2026-06-09 09:03:22 +09:00
dw.jang f00aef50e7 UI #5: 도넛차트 크기 dev/일반 통일 + ml 폰트 키움 + Logs & Charts 일반 모드 노출
회의 피드백:
  - 도넛차트 화면 비율 이상함 → 화면에 꽉 차게
  - 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>
2026-06-08 18:02:44 +09:00
dw.jang 12f1919bc6 UX #4: HOME 진입 시 BLE 연결 유지 + Start 시 설정/Placement skip
회의 피드백: 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>
2026-06-08 17:58:18 +09:00
dw.jang 6c3b88cf2d UI #2: AutoScan Interval → Scan Interval + SEC/MIN 단위 토글
회의 피드백:
  - 제목 '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>
2026-06-08 17:55:18 +09:00
dw.jang e7076ed2a0 UI: 회의 피드백 묶음 1 — Placement 가독성 + Voiding 표현/표시 + catheter 간격
회의 피드백 5건:

#6 PlacementGuide(Sensor Alignment) 글자 가독성
  - Step 1/3 indicator: 14sp MlPrimary → 22sp 검정
  - "Sensor Alignment" 타이틀: 28sp Bold → 36sp ExtraBold (이미 검정)
  - 노란/초록 hint 박스 글자: 20sp hintFg(노랑/초록) → 26sp Bold 검정
    (박스 배경 색은 의미 유지 — 노랑=조정 필요, 초록=Green zone)

#7 Start Alignment 버튼 텍스트: White → Black

#8 도넛차트 Voiding 버튼: "Voiding" → "Voiding / Catheterization"
   (12sp · TextAlign.Center · maxLines=2로 줄바꿈 허용)

#9 도넛차트 mL 표시: Voiding 직후 displayMaxVolumeMl=0 + estimatedVolume=0
   상태에서 "0 mL" 대신 "— mL" 표시 — 측정값이 없음을 명확히

#10 catheter "left" 간격: 2dp → 6dp (숫자와 텍스트 사이 여유)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 17:52:43 +09:00
dw.jang 35887802fd ClinicalHome Unpair: appState.isDeviceConnected를 false로 동기화
증상: 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>
2026-06-08 16:34:33 +09:00
dw.jang fb6f722b53 BLE fix: msn? ASCII " " 포맷 변경 + PiezoMonitoring 진입 시 강제 polling
사용자 검증: 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>
2026-06-08 16:21:41 +09:00
dw.jang 2801d2ae53 Build: productFlavors 추가 — demo(동결 안정판) / dev(개발 진행) + docs 정리
방광 모형 테스트가 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>
2026-06-08 11:49:10 +09:00
dw.jang 95572ac076 BLE fix: 배터리 polling 실제로 polling — 첫 응답 재시도 + 30s 주기
증상: 연결 직후 가끔 배터리 값이 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>
2026-06-05 15:37:23 +09:00
dw.jang 2ae5eecb20 Setup: Auto Scan Interval을 별도 화면(AUTO_SCAN_SETUP)으로 분리
사내 피드백: 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>
2026-06-04 16:15:39 +09:00
dw.jang 9b090a4c8e Setup에 Auto Scan Interval 카드 추가 + BLE 끊김 banner 제거 (silent reconnect)
PiezoPersonalizationView:
  - AutoScanIntervalCard 신규 — 600~5000ms 슬라이더 + ±200ms 스테퍼
    기존 MaxVolume / CatheterThreshold 카드와 동일한 톤
  - 3카드가 화면을 넘칠 수 있어 verticalScroll로 전환, spacing 64→24dp

PiezoMonitoringView / VivamyoMonitoringView:
  - 사내 테스트 피드백: BLE 끊김 banner가 자주 튀어 거슬림. BleManager 측
    scheduleAutoReconnect()가 이미 백그라운드로 재연결을 시도하므로 UI
    알림은 불필요.
  - onUnexpectedDisconnect/onReconnectionFailed/onConnectionStateChanged
    콜백에서 banner state 토글 제거. 측정 일시정지 및 batteryPolling 재시작은
    그대로 유지.
  - 미사용 state (showDisconnectionBanner, reconnectionState) + 관련 import
    정리. BleDisconnectionBanner.kt 파일 자체는 후속 사용 가능성 위해 남김.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 14:50:23 +09:00
dw.jang 0185d55f4b PiezoMonitoring fix: 도넛 색깔(urgency)을 표시값과 동일한 maxVolumeMl 기준으로 통일
증상: Single scan 309ml(노란색) 후 Auto scan에서 trimmed mean 280s가 잡히면
도넛 표시값은 maxVolumeMl=309로 유지되는데 색깔만 초록색으로 떨어지는 불일치.

원인: 도넛 progress/표시값은 maxVolumeMl(세션 최대) 기준이고 PiezoMonitoringView
1098 코멘트도 그렇게 명시하는데, bladderLevel.currentLevel(=urgency=색깔)만
lastVolumeMl(가장 최근 trimmed mean) 기반으로 업데이트되어 source mismatch.

수정: bladderLevel 업데이트도 maxVolumeMl 기반으로 통일. 한 번 도달한 충만도
색깔은 auto scan 노이즈로 인한 작은 측정값 변동에 영향받지 않고 유지됨.
resetLevel(카테터 처치) / 화면 재진입 시에만 초기화되는 기존 동작은 유지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 12:39:55 +09:00
dw.jang 547e834714 Labdb fix: sensor.imu에 IMU FIFO 마지막(최신) 샘플 사용
cycle당 IMU 다수 샘플(FIFO 순)이 오는데, 첫 샘플을 쓰면 piezo 캡처
시점보다 ~240~300ms 전의 오래된 자세값이 labdb에 들어감. 마지막
샘플로 바꿔 piezo 측정 시점과 가장 가까운 자세값이 기록되도록 함.
imu_samples 전체는 그대로 보존.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 11:23:46 +09:00
dw.jang ccd4dcbbc3 Clinical: V4.1 wall detection으로 채널별 ant/post peak 그래프 오버레이
사내 임상 테스트 진행자가 실시간 측정 중 wall peak 위치를 눈으로 확인하고
싶다는 요청 반영. raw 데이터 저장 우선 정책은 그대로 유지하고, 시각화 측면에만
PiezoMonitoring에서 쓰는 V4.1(walldetect.V41Detector) 알고리즘을 사이클마다
호출하여 결과를 채널 그래프 위에 오버레이.

표시:
  - 각 채널 그래프 위 세로선 마커 — ant=green, post=orange (alpha 0.75)
  - 채널 헤더에 'a{antIdx} p{postIdx}' 텍스트 (두 줄 한 칸 차지)
  - V4.1 detection 실패 시(둘 중 하나 null) 마커/텍스트 모두 미표시

동작:
  - 매 mtb 사이클(piezoCollector.onMultiChannelComplete) 시점에 detect 호출
  - SweepInput probe profile은 PiezoMonitoring과 동일 (PiezoHW.presetName/degreeAll)
  - runCatching으로 감싸 detect 실패가 측정 자체에 영향 없도록

저장 데이터는 변경 없음. measurement.json/labdb 업로드 payload는 그대로 raw ADC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 11:13:40 +09:00
dw.jang dadf80a778 BLE: reb 패킷 신구조(210B) 적용 — ch_info 2B + 명시적 채널 식별
펌웨어 업데이트로 reb 응답 패킷이 208B → 210B로 확장됨.
새 payload 앞에 ch_info 2B (= ch_session 1B + ch_num 1B) 필드 추가.

변경 내용:
  reb (old): [tag 4B][num_sample 2B][adc 200B][crc 2B] = 208B
  reb (new): [tag 4B][ch_session 1B][ch_num 1B][num_sample 2B][adc 200B][crc 2B] = 210B

수정 위치:
  - BleManager.kt:970 — 디버그 로그용 sample count 신구조 기반 계산,
    session/ch 정보를 ble.log에 함께 노출
  - PiezoPacketCollector.processRebPacket — 전면 재작성
    * 모든 offset +2 (num_sample → bytes[6..7], ADC → bytes[8..])
    * Endian 감지 첫 ADC 샘플 위치 bytes[8..9]로 이동
    * 채널 저장 방식: 도착 순서(currentChannel++) → ch_num 직접 indexing
    * ch_session 추적 추가 — 같은 session의 6개 reb를 하나의 set으로 묶음
  - processEndPacket(raa:) — 완전성 검사 강화
    * isSetDropped == true → 콜백 안 부르고 폐기
    * channelResults 중 null 존재 (불완전 set) → 폐기
    * 6채널 모두 채워진 경우만 onMultiChannelComplete 호출
    * 폐기/완료 후 currentSetSession=null로 reset → 다음 set 진입 대비
  - red:, ree: — multi-channel 모드에서는 ch_num indexing이라 무시 (single-channel만 유지)
  - currentChannel 필드 제거 (도착 순서 의존 폐기)

호환성:
  - 신버전 펌웨어 전용. 구버전(208B) 단말 미지원.
  - rbb/raa/rim 구조 변동 없음 — 손대지 않음.
  - 온도 센서(TMP235 → IMU register) 변경은 FW 내부일 뿐 클라이언트 포맷 그대로.

무효 패킷 정책:
  - session 불일치 (set 진행 중 다른 session 도착): 이전 set 폐기, 새 set 시작
  - 같은 set 내 duplicate ch_num: isSetDropped=true, raa에서 무콜백 처리
  - invalid ch_num (out of 0..5): set 폐기

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 17:58:38 +09:00
dw.jang c0394f63b7 Clinical: BLE Connected 카드에 Unpair 버튼 추가 — 측정 후 페어링 끊기 편의
PiezoMonitoring의 'Delete' 패턴과 동일: confirm dialog → bleManager.disconnectAndUnbond().
ClinicalHome에 머무르므로 곧바로 BLE 카드가 Not Connected 상태로 전환되어
다음 사람 측정 전에 Connect 버튼 활성화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 17:46:54 +09:00
dw.jang 010e9acfbd Labdb fix: payload sanitize for PostgreSQL JSONB compatibility
서버 로그에서 두 원인 확인:
  [error] POST /devices/register invalid byte sequence for encoding UTF8: 0x00
  [error] POST /upload/json    unsupported Unicode escape sequence

PostgreSQL JSONB는 NULL escape를 거부 (HTTP 500). BLE rid: 응답에서
US-ASCII 디코딩 후 .trim()은 whitespace만 제거하고 NULL/제어문자는
남기므로, firmware/hw/serial 문자열에 padding NULL이 묻은 채 payload로 직렬화됨.

두 군데 방어:
  1. BleManager.rid: 파싱에서 0x00~0x1F,0x7F를 공백으로 치환 후 trim
  2. LabdbClient.sanitizeForJsonb 추가 — register/uploadJson 직전에
     payload를 재귀적으로 순회하며 모든 string에서 C0 control + DEL 제거
     (탭/개행/캐리지리턴은 보존)

원인 조사: 서버 SSH로 /var/log/labdb.log 직접 확인 +
server.js의 /upload/json 핸들러 코드 검토 결과 서버는 받은 sensor/channels
JSON을 그대로 JSONB 컬럼에 INSERT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 17:30:19 +09:00
dw.jang b7475a3b6f Labdb: debug logging — register payload + HTTP 응답 body 출력
INTERNAL_ERROR(500) 원인 좁히기 위해 LabdbClient에 logcat 추가:
- POST /devices/register 보내는 정확한 payload (TAG=LabdbClient)
- 모든 응답의 HTTP code + raw body (앞 500자)
- IOException 발생 시 스택 trace

logcat 필터: tag:LabdbClient

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 14:41:27 +09:00
dw.jang 3b96e5b4cb Clinical: dataType "001" (내부 임상) + measurement.json/meta.json에 data_type 필드 노출
labdb 자료 구분 코드를 "000"(외부 일반 VesiScan) → "001"(내부 임상 R&D)로
변경. ClinicalSessionStore에 const DATA_TYPE을 두고, meta.json과
measurement.json의 meta 블록에 "data_type": "001"을 함께 기록 — 폴더만
보고도 내부 임상 데이터임을 즉시 식별 가능.

LabdbUploader는 measurement.json의 meta.data_type을 우선 사용하고
없으면 "001" fallback. 향후 dataType이 늘어나도 단일 진실 출처(meta)에서
파생되도록 분리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 12:34:18 +09:00
dw.jang b3d9c16780 Clinical: labdb.medithings.net REST 업로드 통합 (Phase A+B)
End Session 시 measurement.json을 https://labdb.medithings.net/api/v1/upload/json
에 자동 업로드. dataType="000" (VesiScan 6채널). cycle 1개 = record 1개.
testId는 폴더명 그대로 사용 (40자 초과 시 prefix+hash로 축약).

Files:
  - LabdbCredentials: apiKey/deviceId를 EncryptedSharedPreferences에 안전 저장
  - LabdbClient: register/checkStatus/uploadJson + 5가지 에러 분기
    (PendingApproval / Reregister / RateLimit / ApiError / Network)
  - LabdbUploader: measurement.json → payload 변환 + fire-and-forget 업로드
    성공 시 labdb_upload.json, 실패 시 labdb_upload_error.json 마커 작성

ClinicalSessionStore.endMeasurement: cycle>0 + active 상태일 때만 자동 트리거.
ClinicalHomeView: labdb 상태 카드(Register/Refresh) + 직전 측정 업로드 상태
표시 + 수동 Upload 재시도 버튼.

오프라인 큐는 아직 없음 (Phase C는 후속).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 12:31:56 +09:00
dw.jang 6f9ce00c65 Clinical: measurement.json 통합 저장 (사이클 누적 + End 시점 finalize)
ClinicalSessionStore에 MeasurementCycle 누적 버퍼 추가. End Session 시
6채널 piezo + 15 IMU 샘플을 단일 measurement.json으로 묶어 저장하여
분석 PC에서 한 줄(json.load) 로드 가능. 기존 CSV 로거는 유지.

ClinicalLiveView는 pendingPiezo 패턴으로 piezo(reb×6+raa)와
imu(rim)를 한 cycle로 묶어 addCycle() 호출.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 12:18:43 +09:00
dw.jang 38fa93c8b2 feat: 임상 Live 6채널 시각화 화면 (ClinicalLiveView)
vesiscan_test Section 2 프로토콜 패턴을 안드로이드 앱 안에서 직접 구현.
실제 몸에 센서 붙인 채 6채널 실시간 그래프 보면서 측정 + 저장 가능.

신규 화면 ClinicalLiveView:
- 600ms 간격 mtb? 자동 송신 (Piezo 6ch + IMU 동시)
- 2×3 grid 라이브 차트 (raw ADC waveform, CH0~CH5 색상별)
  · y축 0~4095 (12-bit ADC) 고정 + 가로 grid 4분할
  · 각 채널 peak 값 표시
- IMU 카드 — ImuAnalysis 기반 자세 + tilt + shake (흔들림 시 빨강 경고)
- 상단: 세션 라벨 + S=Subject TV=TrueVol Ab=Abdomen 한 줄 + #cycle ✓captured
- 컨트롤: Pause/Resume · Capture (수동 저장) · Auto Capture 토글 · End Session

ClinicalSession 모델 확장:
- subjectId / trueVolumeMl / abdomenThicknessMm 필드 추가
- folderName 에 subjectId prefix (예: "S001_VBT26050202_SUPINE_ALIGN_0CM_...")
- meta.json 에 subject_id / true_volume_ml / abdomen_thickness_mm 자동 기록

ClinicalSessionStore:
- lastSubjectId / lastTrueVolumeMl / lastAbdomenThicknessMm 기억 (다음 측정 자동 채움)

ClinicalHomeView 입력 폼 확장:
- Subject ID 텍스트필드
- True Volume (mL) + Abdomen (mm) 2-컬럼 숫자 입력
- Start Live Measurement 버튼 → CLINICAL_LIVE 화면으로 직행
  (이전: alignment/BV step에 따라 PlacementGuide/PiezoMonitoring 분기)

AppState/MainActivity:
- AppScreen.CLINICAL_LIVE 추가
- MainActivity Back: LIVE 시 endMeasurement + ClinicalHome 복귀

데이터 흐름:
  Start Live → ClinicalLiveView 진입 → mtb? 600ms loop
    → piezoCollector → 6채널 buffer (자동 또는 수동 capture)
    → AdcCsvLogger.log → adc.csv 누적
    → imuCollector → 15 IMU samples
    → ImuCsvLogger.log → imu.csv 누적
  End/Back → endMeasurement → meta.json finalize → ClinicalHome 복귀

장점:
- 검사자가 실제 시연 중 6채널 신호 품질 즉시 확인
- Auto Capture로 매 사이클 자동 저장 (Pause로 일시 멈춤)
- subject_id 기반 폴더명으로 피험자별 grouping 자연스럽게

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 11:17:55 +09:00
dw.jang ec3be6fa5d feat: Sensor Alignment에 IMU 통합 (mtb? + rim: 정식 처리)
PPT 'VesiScan-Basic IMU FW 진행사항_260521' 사양 기반 구현.
실측 1회 (2026-05-27 12:24, 평평한 책상)로 endian/필드 순서/시간 순서 확정 후 정식 적용.

확정 사양 (실측):
- rim: 패킷 188B = [tag 4B "rim:"] [num 2B BE] [data 180B = 15 samples × 12B] [crc 2B]
- 1 sample = 12B: Ax 2B + Ay 2B + Az 2B + Gx 2B + Gy 2B + Gz 2B (모두 BE int16)
- Accel ±4g sensitivity: /8192 g/LSB
- Gyro ±500 dps sensitivity: /65.536 dps/LSB
- 샘플 순서: samples[0] = oldest (FIFO 순)

신규 파일:
- ble/ImuPacketCollector.kt — rim: 파서. ImuSample data class + 15-sample list 콜백
- services/ImuCsvLogger.kt — Clinical 모드 한정 imu.csv 자동 저장
  컬럼: scan_id, timestamp, clinical_label, firmware_version, sample_idx, ax_g, ay_g, az_g, gx_dps, gy_dps, gz_dps
- managers/ImuAnalysis.kt — 자세 추정/Tilt/흔들림 RMS 유틸 (간이, 임상 보정 필요)

수정:
- BleManager: imuCollector 필드 + sendMtb() 정식 명령
  (canSendMaa() 게이트 maa와 공유. piezo + IMU 동시 캡처)
- BleManager: rim: 핸들러 정식화 (imuCollector.parseRim 위임)
- PlacementGuideView: sendAllChannels → sendMtb 교체, imuCollector.onComplete 등록
  Clinical 모드일 때 ImuCsvLogger.log() 자동 호출
  Dev 모드 한정 상단 칩에 "IMU: Supine · tilt=N° · shake=N.Ndps" 표시

데이터 흐름:
  performScan → sendMtb? → 응답:
    1) reb × 6 (Piezo, 기존 처리)
    2) raa (Piezo end, 기존 onMultiChannelComplete 트리거)
    3) rim (IMU, 신규 imuCollector.onComplete → lastImuSamples 갱신 + imu.csv)

분석:
- ImuAnalysis.estimatePosture: Accel 평균으로 Supine/Sitting/Standing/Tilted/Moving 판정
- shakeRmsDps > 30 → "Moving" (자세 판정 보류)
- tiltDegrees: z축 vs 중력 각도

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:19:20 +09:00
dw.jang 020a45ba92 feat: 펌웨어 버전을 모든 로그 파일에 직접 노출
기존: meta.json에만 기록 (파일 단독으로 보면 펌웨어 확인 불가)
변경: adc.csv 컬럼 / placements.jsonl·events.jsonl 필드 / ble.log 헤더 블록 모두 노출

검사자/분석자가 각 파일 단독으로 열어도 어느 펌웨어 데이터인지 즉시 확인 가능.

변경:
- BleManager.current: companion에 instance accessor 추가 (logger 같은 context-free 컴포넌트에서 BLE state 읽기용)
- BleManager rid: 핸들러에서 debugLogger.deviceInfo(fw, hw, sn) 호출 (Headers 블록 기록)
- AdcCsvLogger: CSV 헤더에 firmware_version 컬럼 추가, 모든 행에 값 채움
  scan_id,timestamp,clinical_label,firmware_version,volume_ml,lr_ratio,...
- MeasurementLogService.logMeasurement / logPlacement: JSON에 firmware_version 필드 추가
- BleDebugLogger.deviceInfo(): 새 메서드. rid: 응답 받은 후 ble.log에 다음 블록 기록:
  ─── Device Info ───
  Firmware:  VBTFW0116
  Hardware:  VBTHW0100
  Serial:    VBT26040001
  ───────────────────

BLE 미연결 / mid 응답 안 옴 시엔 필드/값 비어있음 (정상). 응답 받자마자 자동 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:19:18 +09:00
dw.jang de3177e5e0 feat: ClinicalSession meta.json에 firmware/hardware/serial 자동 기록
R&D 분석 시 펌웨어 버전별 데이터 grouping 가능 (VBTFW0111 vs VBTFW0116 등).

writeMeta()에서 BleManager 인스턴스의 firmwareVersion/hardwareVersion/serialNumber
state를 읽어 meta.json에 기록:
- "firmware_version": "VBTFW0116"
- "hardware_version": "VBTHW0100"
- "serial_number": "VBT26040001"

값이 비어있으면(연결 안 됨/응답 안 옴) 필드 생략. 측정 종료 시점(endMeasurement)에
한 번 더 writeMeta 호출되므로 측정 중 BLE 응답 받으면 자동 갱신됨.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:09:22 +09:00
dw.jang e1fa0d1620 feat: 펌웨어 버전 앱 내 표시 (Settings + ClinicalHome)
연결 시 자동으로 받던 mid?/rid: device_info를 UI에 노출. 이전에는 BLE 디버그 로그에만
기록되어 사용자가 접근 불가했음.

BleManager:
- firmwareVersion / hardwareVersion / serialNumber 3개 MutableState 추가
- rid: 응답 파싱 — "VBTHW0100   VBT26040001 VBTFW0111" 형식을
  공백 split + prefix 매칭(VBTHW/VBTFW/VBT)으로 안전 추출
- disconnect 시 3 필드 모두 clear

PiezoMonitoringView Settings overlay:
- 기존 "Version" 행을 "App" + "Firmware" 두 행으로 분리
- FW 없으면 "—" 표시 (연결 대기 중)
- dev 모드 한정 "Serial" 행 추가 (회사 자산 추적용)

ClinicalHomeView BLE 연결 카드:
- deviceName 아래 "FW: VBTFW0116" 한 줄 추가
- 검사자가 별도 클릭 없이 펌웨어 버전 즉시 확인 가능

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:41:59 +09:00
dw.jang 3b8fc9ca5d UI: Sensor Alignment 방향 화살표 4종 복원 (사용자 요청)
빌드 111(e0f92eb)에서 의도적으로 제거했던 4방향 화살표(↑↓←→)를
사용자 요청으로 복원. directionIcon에 따라 센서/타원 바깥에 그려짐.
Hint 텍스트와 함께 시각적 방향 안내 제공.

색상 #FF5722 · arrowLen 70f · sw 9f (Charles 833c110 원본 사양 그대로)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:51:40 +09:00
dw.jang 76bb0942c0 chore: version bump 1.0.0-design → 1.1.0-design (versionCode 25)
v1.1 변경 요약 (1.0 이후):
- Clinical R&D Mode 신규 (Dev 한정)
- UI 3-phase 리디자인 (Critical/Important/Polish 12건)
- 측정 로그 자동 저장 (adc.csv / placements.jsonl / ble.log / meta.json)
- 외부 저장소 영구 보관 (Downloads/VesiScan_Sessions/, _Events/)
- Auto Scan interval 사용자 조절 가능 (600~5000ms)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:40:36 +09:00