Commit Graph

62 Commits

Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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 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 e370d9c702 PERF: 측정 사이클 간격 1500/1000ms → 600ms 단축
MTU 247 + CONN_PRIORITY HIGH 적용 후 한 사이클 응답이 ~280~420ms로 단축됨
(이전 ~1.2~1.4s 대비 약 4배). 응답 시간 대비 과도하게 보수적이었던
throttle/loop delay를 일괄 단축.

변경:
- BleManager.canSendMaa() 시간 게이트 1500ms → 600ms (응답 평균 + 안전 마진 ~200ms)
- BleManager.canSendMaa() FORCE 게이트 5000ms → 3000ms (응답 빨라진 만큼 deadlock 감지도 빠르게)
- PiezoMonitor Auto loop delay 1500ms → 600ms
- PlacementGuide continuous scan loop delay 1000ms → 600ms

state-based gate(!isComplete)가 안전망 역할을 하므로 응답이 늦어지면
자동으로 차단되어 데이터 유실 위험은 그대로. 정상 경우엔 사이클당 ~0.6~0.7s로
실시간 모니터링 반응성 2배 향상.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:34:51 +09:00
dw.jang c1fac51085 BLE: 연결 직후 CONNECTION_PRIORITY_HIGH 요청
VBTFW0116 펌웨어가 pending slot 1→8로 확장해 TX queue 포화 시
패킷 보관/재전송으로 ADC 유실을 줄였음. 안드로이드 측에서는 Central의
Link Layer ACK 속도를 끌어올려 펌웨어 TX queue 포화 빈도 자체를 낮추는
방향이 가장 효과적.

CCCD write 완료(서비스 ready 시점)에 gatt.requestConnectionPriority(HIGH)
호출. peripheral이 수락하면 connection interval이 ~15ms로 협상되어
한 사이클 응답 시간 단축 가능. 거부되어도 패널티 없음.

실제 적용 여부는 sniffer로만 확인 가능. logcat의 "CONN_PRIORITY HIGH
requested (ok=...)" 로 호출 성공 여부만 확인 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:34:51 +09:00
dw.jang cac3712b8c BLE: maa throttle을 state-based 게이트로 강화
이전 응답이 완료되기 전에 새 maa가 송신되면 piezoCollector.startMultiChannel(6)이
호출되며 collector가 reset → 큐에 떠다니던 reb 패킷이 폐기되어 BV_FAIL/BV_SKIP가
발생하던 문제 대응.

변경:
- 시간 단독 게이트(800ms) → state + 시간 복합 게이트
- (A) piezoCollector.isMultiChannel && !isComplete 이면 차단
       단 5초 넘으면 강제 통과(영영 deadlock 방지)
- (B) 최소 간격 800ms → 1500ms (펌웨어 응답 시간 ~1.2~1.4s 반영)
- sendAllChannels/sendChannelsOnly가 공유하는 canSendMaa() 헬퍼로 일원화

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:34:51 +09:00
dw.jang 6287f21a05 fix: 센서 비율 기반 + LED 깜빡임 + 디바운스 + gated 체크
[Placement]
- 센서 이미지 비율 기반 (너비 35%, BiasAlignment 0.44) — 태블릿 대응
- LED 깜빡임 수정: greenHeld/failCount<3 동안 LED 5 안 보냄
- 디바운스: phaseProtected 중 lastHintChangeMs 안 건드림 (화살표 안 나오던 버그)
- Step 2 전환 힌트 항상 "Checking left/right balance..."
- GREEN에서 ch4/ch5 개별 탈락 시 방향 안내 (lrDev NaN 처리)

[도넛차트]
- Fill 카드: 윗줄 65%, 아랫줄 325/500 mL
- ml → mL 통일
- Spot 완료 시 displayMaxVolumeMl 즉시 갱신
- Auto 윈도우 < 5일 때 Current Measurement "—"

[기타]
- V41 gated 체크 + 최소 len≥3 (ungated 피크 안 보임)
- CCCD 실패(status 133) 500ms 후 1회 재시도
- 기기 스캔 사전순 정렬
- 힌트 점 순환 (./.../...)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-11 10:07:15 +09:00
dw.jang 38ab74369e UI: 8건 피드백 반영 + Spot 봉인 + 페어링 체크
[Placement]
#1 센서 위치 타원 중앙 (offset y=65dp)
#5 skin 색 항상 동일 (greenZone 분기 제거)
#6 화살표 센서 밖으로 (sensorHalfH + 20f 간격)
#8 GREEN 체크마크 제거
센서 크기 192x120dp (80%)

[Donut]
#2 상단 버튼 1.5배 (48/56dp)
#3 배터리 아이콘 Canvas 그리기 (충전량 바)
#4 도넛-카드 간격 2배 (32/40dp)
#7 Bladdy 정중앙 (matchParentSize + Arrangement.Center)

[기능]
- Spot 중 Auto/Void 버튼 봉인 (enabled=false)
- BLE 페어링(bonding) 완료 확인 후 다음 화면 (30초 대기)
- R-peak gate Gradient 모드 비활성화
- GREEN 디버그 로그 (cv/lrPass/rPeakOk)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 17:27:34 +09:00
dw.jang 758930796f fix: critical code review — thread safety, state management
CRITICAL fixes:
- #1: lastMultiChannelResult 변경을 handler.post로 (메인 스레드 보장)
- #3: measureDoneSignal var=remember → val mutableStateOf (recomposition 안전)
- #34,35: BleManager bluetoothGatt/tx/rxCharacteristic/lastRxTimestamp @Volatile
- #66: GreenZoneConstants 모든 mutable var @Volatile
- #56: PiezoHW.distancePerSample @Volatile
- nested handler.post 제거 (불필요한 프레임 지연)
- maa throttle deadlock 방지 (Boolean 반환 + state 리셋)
- Plot/Log 불일치 수정 (channelLogs를 allWalls 기준으로 통일)
- Placement 센서 위치/화살표 조정, gradient dead zone, CV relaxation
- 개발자 모드 CV 표시, 가이드 상자 항상 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 16:25:06 +09:00
dw.jang 414ce940a2 fix: maa 1초 throttle — BLE 계층에서 이중 전송 물리 차단
handler.post로 isMeasuring 갱신해도 Compose state 레이스 해결 안 됨.
BleManager에서 lastMaaSentMs 타임스탬프로 1초 이내 재전송 무조건 차단.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 12:27:58 +09:00
dw.jang 54bca199af fix: maa 이중 전송 방지 (BV_FAIL 근본 원인)
- isMeasuring=false를 handler.post로 메인 스레드 갱신
  (Thread 내 설정 → Compose state 지연 → 다음 measure() 통과 → maa 이중 전송)
- measure() 진입 시 isMeasuring 가드 추가 (중복 호출 방어)
- 채널 수신 로그 추가 (RECEIVED N channels)
- MTU 247 negotiation 추가 (reb: 208B 분할 전송 최소화)
- Spot 6ch 필터 추가 (allChannelsReceived)
- Auto Stop 시 값 유지 (재계산 안 함)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-08 11:59:13 +09:00
dw.jang 258bad9a26 feat: Foreground Service + Detachment 개선 + Placement 가이드 모드
Foreground Service (BleForegroundService.kt):
- BLE 연결 시 자동 시작, 해제 시 자동 중단
- PARTIAL_WAKE_LOCK (최대 4시간)
- Notification: "VesiScan - BLE 기기 연결 중..."
- connectedDevice foregroundServiceType
- Doze 모드에서도 BLE 통신 유지
- keepScreenOn도 유지 (추가 안전장치)

Detachment 개선:
- THR_MEAN/STD 100→30 (완전 탈착만 감지, 거리 약화 무시)
- 6채널 모두 수신됐을 때만 체크 (BLE 거리 문제와 구분)
- Placement: if(false) 제거 → 활성화 + "센서가 부착되지 않았습니다"
- Monitoring: 정상 복귀 시 LED OFF

Placement 가이드 모드:
- Simple (기본): 3+채널 → 바로 좌우 (기존)
- Boundary: 올라갔다 내려오기 패턴 (경계 감지 → 최적점 복귀)
- 설정 톱니바퀴에 Simple/Boundary 스위치

BLE 로그:
- 역순 표시 (최신이 위) — 스크롤 위치 유지

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 16:09:54 +09:00
dw.jang 85162669e4 fix: BLE 패킷 수신 메인스레드 의존 제거 + Auto 간격 1.5초
BLE:
- onCharacteristicChanged에서 handler.post 제거
- processReceivedData를 Binder 스레드에서 직접 실행
- 메인 스레드 큐 대기로 인한 reb 패킷 유실 방지

Auto:
- 최소 간격 1000ms → 1500ms (기기 연속 maa 채널 드롭 방지)
- 10개 × 1.5초 = ~15초마다 trimmed mean 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:37:14 +09:00
dw.jang 1a42fe6a07 fix: 분석 백그라운드 Thread, 그래프 빈칸 표시, CSV 세션별 분리
성능:
- 측정 콜백 분석(TGC/Otsu/BV)을 백그라운드 Thread로 이동
- UI 업데이트만 handler.post로 메인 스레드 복귀
- 메인 스레드 블로킹으로 인한 BLE 패킷 드롭 방지
- 분석 소요시간 로그 출력 (Analysis took Xms)
- Toast도 handler.post로 감싸서 Thread-safe

Placement:
- 6채널 그래프: 데이터 없는 채널도 "No data" 빈 칸 표시 (항상 6칸)

CSV:
- 일별 파일 → 세션별 파일 (YYYY-MM-DD_HHmmss.csv)
- BLE 연결 시 자동 newSession()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 10:04:03 +09:00
dw.jang c6e84bd6c0 fix: watchdog heartbeat, 배터리 계산, placement 디바운스+방향 수정
BLE:
- watchdog idle heartbeat: 10초 무통신 시 IMU query로 keepalive
- 배터리: 3500-3700mV→0-5%, 3700-4100mV→5-100%, 증가 무시
- 배터리 표시: 아이콘→퍼센트 숫자 (색상 유지)

Placement:
- 힌트 디바운스: 같은 phase 2회 연속일 때만 변경 (깜빡임 방지)
- 방향 힌트 반전: CH3만 잡힘→아래로↓, CH0만 잡힘→위로↑

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 17:27:32 +09:00
dw.jang 5cdae8a86e fix+UX: BLE 중복수신 수정, peak detection 개선, UI 대폭 정리
BLE:
- GATT 다중연결 race condition 수정 (reconnectConnecting 플래그)
- onCharacteristicChanged API 레벨 분기 (dual callback 방지)
- DeviceScan: isConnected → isServiceReady 전환 (서비스 준비 후 화면전환)
- Placement: mpa/maa 500ms 딜레이 (power on 대기)
- Disconnect & Unbond 기능 추가 (msr? + removeBond + 저장정보 삭제)
- autoConnect 삭제 (HomeView)

Algorithm:
- 후위벽 피크 >= threshold 조건 추가 (false positive 차단)
- low-echo 분석범위 80샘플 제한 (뒤쪽 노이즈 무시)
- urineLen: post-ant-1 → post-ant 수정
- Placement: UrinAI fallback 제거 (PiezoEchoAnalyzer만 사용)
- 기기 프리셋 autoDetect index 7→8 수정 (VBT26040Nxx)
- dps 기본값 1.771 → 1.968

UI:
- 도넛차트: Home버튼 삭제, Auto/Catheter count/History 삭제
- 3버튼 레이아웃 (Catheterize/Auto/Spot)
- 배터리 아이콘 (60+초록/30+주황/빨강) + 설정 톱니바퀴 분리
- SensorSelect/Consent 페이지 스킵
- 온보딩: VesiScan-Basic, NIRS 페이지 삭제
- MaxVolume: Step 표시 삭제, mL 오른쪽 배치
- BackHandler 전체 재정리 (자연스러운 뒤로가기)
- BLE 신호 기준 완화 (Good≥-80, Fair≥-90)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 17:03:37 +09:00
dw.jang 227480af4b feat: UI 일괄 수정 (5,6,7,8번)
5. Onboarding 간소화: 4단계→1단계 (Max Volume만)
   Step 1(자세)/2(위치)/4(최적화) 제거, Continue 바로 완료

6. 자동 reconnect: 앱 시작 시 마지막 기기에 자동 연결
   SharedPreferences에 address+name 저장, HomeView에서 autoConnectLastDevice()

7. Demo 모드: DeviceScanView에서 Demo 버튼 제거

8. Android 뒤로가기: BackHandler로 화면별 네비게이션 매핑
   Placement→Home, Monitoring→Placement, History/Diary→Monitoring 등

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 11:41:22 +09:00
dw.jang 9d171f0e06 fix: 배터리 연결 시 1회만 측정 (반복 폴링 없음)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 18:25:18 +09:00
dw.jang 953e6b56aa fix: 배터리 폴링(5초 msn) 제거 — 측정 간섭 방지
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 18:24:27 +09:00
dw.jang 737eaa1a32 fix: mcs 명령 제거 (avg=10이 echo detection 방해 가능성 — 기기 기본값 사용)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 17:58:26 +09:00
dw.jang 26d5ae20fd fix: mcs avg=10으로 설정 (freq=2, cycles=5, avg=10, delay=20, samples=140)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 17:53:52 +09:00
dw.jang 4045ae5198 fix: 연결 시 mcs avg=3 복원 (이전 avg=10 설정이 기기에 남아있음)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 17:45:37 +09:00
dw.jang fecb02dfde fix: Endian 감지 간헐적 실패 수정 (19회 중 5회 ADC 값 폭발)
근본 원인: CH0 첫 샘플이 LE/BE 둘 다 4095 이하일 때
  감지 불가 → LE로 잘못 판정 → 전체 채널 값 폭발 (30000~65000)

수정:
- 확실한 경우: 기존 로직 유지 (LE>4095→BE, BE>4095→LE)
- 애매한 경우: forceBigEndian 기본값 사용
- VBT* 기기: forceBigEndian=true (기기 연결 시 자동 설정)
- 2025MEDIP: forceBigEndian=false
- Endian 결정 로그 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 12:09:57 +09:00
dw.jang 208de17152 fix: 모든 BLE 명령어를 테스터 exe와 동일 포맷으로 통일
테스터(vsbtester) buildPacket() 분석 결과:
- 파라미터 있는 명령: 16비트 Big Endian (buildCommandBE)
- 파라미터 없는 명령: 공백 0x20 추가 (buildCommandASCII " ")

변경:
- mpa, mec, msn, mta, mls → buildCommandBE (BE 파라미터)
- mps, maa, mqq, mag, mcj, mid, msp, mbb → buildCommandASCII " " (공백)
- maa sendChannelsOnly도 공백 방식으로 통일 (기존 LE [0] 제거)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:42:02 +09:00
dw.jang fdc4047c5c fix: mls LED 명령 Big Endian 파라미터로 변경 (테스터 exe와 동일)
테스터 코드 분석 결과:
  exe: mls? + [0x00, 0x05] (16비트 Big Endian) → LED 바뀜
  앱: mls? + [0x05, 0x00] (16비트 Little Endian) → LED 안 바뀜

CRC16.buildCommandBE() 추가: 파라미터를 Big Endian으로 직렬화
sendLedMode(): buildCommandBE("mls", [state]) 사용

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:38:35 +09:00
dw.jang 3cebaff6a0 fix: mls 명령 포맷 'mls? 5' (공백+ASCII) 시도
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:28:29 +09:00
dw.jang e6e9c296f2 fix: mls LED 명령 ASCII 포맷으로 변경 (buildCommand→buildCommandASCII)
기존: mls? + [0x05, 0x00] (2바이트 LE 파라미터) → LED 안 바뀜
변경: mls?5 (ASCII 숫자) → exe 프로그램과 동일 포맷

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 16:04:44 +09:00
dw.jang 76ec271352 fix: msp/mbb 명령어 뒤에 공백 추가 (mid?와 동일 포맷)
기기가 파라미터 없는 명령에도 공백을 요구함 (펌웨어 파서 특성)
- sendImuQuery: buildCommand("msp") → buildCommandASCII("msp", " ")
- sendFullMeasurement: buildCommand("mbb") → buildCommandASCII("mbb", " ")
- IMU 1초 쿼리 재활성화

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 13:49:30 +09:00
dw.jang 9842644e23 fix: mbb 측정 복원 — PiezoPacketCollector가 rbb: 헤더를 skip하도록 수정
근본 원인: mbb? 응답에서 rbb: 헤더가 먼저 오는데 collector가 모르고 무시
→ onMultiChannelComplete 콜백 안 불림 → isMeasuring 영구 true → 버튼 비활성화

수정:
- PiezoPacketCollector.addPacket(): rbb: prefix 인식 → skip + 로그
- BleManager: rbb: 수신 시 piezoCollector.addPacket()에도 전달
- PiezoMonitoringView: sendChannelsOnly→sendFullMeasurement(mbb) 복원

흐름: mbb? → rbb:(skip) → reb:×6(수집) → raa:(완료) → 콜백 정상

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 12:14:27 +09:00
dw.jang bf2f16b207 fix: Spot 측정 mbb→maa 복원 (rbb 헤더가 collector 혼동 유발)
mbb 응답(rbb:→reb:×6→raa:)에서 rbb: 헤더가 piezoCollector를
혼동시켜 그래프가 안 뜨는 문제 발생.

- Spot 단일 측정: sendChannelsOnly(maa) 사용
- 자동 측정 10초째: measure()→maa 사용 (mbb는 파싱 검증 후 전환)
- IMU 전송 시 isMeasuring 체크 추가 (측정 중 충돌 방지)
- sendChannelsOnly() 함수 추가 (기존 sendAllChannels와 동일)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 12:09:02 +09:00
dw.jang af6071e18b fix: mid? 명령 포맷 변경 — buildCommandASCII("mid", " ") 공백 포함
기기에서 rxs:mid? (unknown command) 응답 → 포맷 문제 가능성
buildCommand("mid") → buildCommandASCII("mid", " ") 로 변경하여 테스트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:45:47 +09:00
dw.jang eb5affc6b8 feat: 자동측정 IMU 1초 + 전체측정(mbb) 10초 루프
자동측정 변경 (기존: 30초 maa → 신규: 1초 msp + 10초 mbb):
- 1~9초: msp(IMU query) → rsp(accel+gyro) 1초 간격
- 10초째: mbb(전체측정) → rbb(헤더) + reb×6 + raa
- 기존 maa → mbb(sendFullMeasurement)로 전환
- mbb 응답은 rbb: 헤더 후 reb:/raa: 동일 파이프라인

BLE 명령어 추가:
- sendImuQuery(): msp 전송
- sendFullMeasurement(): mbb 전송 (piezoCollector 연동)
- sendLedMode(state): mls 전송

응답 파싱 추가:
- rxs: → "cmd_not_supported" (mid? 미지원 기기 대응)
- rbb: → full measurement header
- rsp: → IMU data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 11:31:03 +09:00
dw.jang e6e339379b feat: ADC 로그 저장 + 기기정보 mid? + LED 모드 + 무한 3초 reconnect
1. ADC 로그: 측정 완료 시 MeasurementLogService.logMeasurement() 호출
   → 6채널 raw ADC + 채널별 ant/post/score 저장 (JSONL)

2. 기기 정보: 연결 직후 mid? 전송 → rid: 응답 파싱 → 로그 기록
   (HW Version, Serial Number, FW Version 일괄 조회)

3. LED 모드: Placement Guide 연동
   - 스캔 시작 → mls?(5) ALIGN_SEARCHING
   - PASS → mls?(6) ALIGN_COMPLETE
   - 스캔 종료 → mls?(0) OFF

4. Reconnect: 3/5/10초 5회 → 3초 간격 무한 재시도
   (연결됐다가 끊어진 경우, 사용자가 Home 돌아갈 때까지)

5. BLE 로그: mid/mls TX 파싱, rid:/rls: RX 파싱 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 10:55:29 +09:00
dw.jang df42a47d7d feat: BLE 로그에 파싱된 값 추가 (TX 명령어 + RX 응답 의미)
TX: "maa 6ch measure", "msn battery query", "mpa piezo power ON" 등
RX: "rsn battery=3850mV (100%)", "reb 100samples", "raa all-ch complete",
    "rpa piezo ack", "rag gain calibration (24 values)" 등

펌웨어팀에 보낼 때 raw hex 없이도 통신 흐름을 이해할 수 있음

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:40:23 +09:00
dw.jang 796bce19a2 cleanup: WATCHDOG_CHECK 디버그 로그 제거 (이벤트성 로그만 유지)
WATCHDOG_STARTED/TIMEOUT/GATT_ERROR/RECONNECT 등 이벤트 로그는 유지.
5초마다 찍히던 WATCHDOG_CHECK/THREAD 상태 로그 제거.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:35:19 +09:00
dw.jang a474935e3c fix: GATT 에러로 끊길 때도 자동 reconnect 시작 (핵심 버그 수정)
근본 원인: Android BLE가 disconnect 시 STATUS_DISCONNECTED가 아닌
GATT 에러(status≠0)로 콜백을 보냄. 기존 에러 경로에서는
reconnectAttempt>0일 때만 재연결 → 첫 끊김 시 재연결 안 됨.

수정:
- wasConnected 체크: 이전에 연결됨 + 사용자 disconnect 아님 → 자동 reconnect
- GATT_ERROR 로그 + DISCONNECTED(gatt_error) 로그 기록
- stopBatteryPolling/stopWatchdog 호출 추가
- watchdog: connected=false 감지 시 무한 루프 대신 break

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 15:13:11 +09:00
dw.jang d3006d91dc debug: watchdog Thread 매 체크마다 로그 기록 (WATCHDOG_CHECK)
watchdog이 실제로 돌고 있는지, 어디서 멈추는지 진단용.
매 5초: WATCHDOG_CHECK silence=Xms connected=true/false
interrupt 시: WATCHDOG_THREAD interrupted
flag 종료 시: WATCHDOG_THREAD stopped

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:59:07 +09:00
dw.jang c99eb068a5 fix: watchdog GATT close + 로그를 watchdog Thread에서 직접 실행
기존: handler.post 안에서 로그+disconnect → handler 블로킹 시 둘 다 안 됨
변경:
- debugLogger.error() → watchdog Thread에서 직접 호출 (CopyOnWriteArrayList 스레드 안전)
- gatt.disconnect()/close() → watchdog Thread에서 직접 호출 (GATT는 any thread OK)
- UI 업데이트 + reconnect만 handler.post로 전달

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:39:54 +09:00
dw.jang 7ab3d878e7 fix: watchdog 누락 경로 수정 + 시작 로그 추가
- CCCD 없는 경로(no CCCD, no RX)에서도 startWatchdog() + startBatteryPolling() 호출
- watchdog 시작 시 WATCHDOG_STARTED 로그 기록 (실제 동작 확인용)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 14:22:40 +09:00