docs: v7 (2026-07-10) 반영 — msp 제거, 6-stage alignment, mtb queue fix, labdb 자동 재시도

이번 세션 대량 변경 사항을 5개 문서에 반영. 각 문서마다 stale 이던
섹션을 갱신하거나 신규 섹션 추가.

USER_GUIDE.md
  - Sensor Alignment 를 V1 (3-stage) / V2 (6-stage) 로 재구성
  - V2 6-stage 표 + relaxed mode / soft hint 설명
  - GREEN 진입 5초 hold + 10-strike 리셋 완화 명시
  - "최적의 위치입니다!" 문구 반영

docs/BLE_PROTOCOL_REFERENCE.md
  - msp 명령 취소선 처리 + mim 신규 명령 문서화
  - Watchdog timeout 25초 연장 명시 (2.5)
  - §2.6 신규: firmware VBTFW0121 mls mode 0 freeze 취약점 +
    앱 측 3-layer 회피 (isMtbBusy / mtb 3초 timeout / 자동 재연결)

VesiScan_Android_Pipeline_Summary.md
  - v7 (2026-07-10) 섹션 신규 추가 — BLE / Alignment / UI / labdb /
    tools 5개 카테고리로 변경 사항 정리
  - 권장 펌웨어 표기 VBTFW0116 → VBTFW0120+ 로 갱신

labdb.md
  - §12b 신규: 앱 측 Auto Retry Policy — endMeasurement 자동 업로드
    조건 완화, LabdbAutoRetry object, UI 배너, 재시도 안전성

tools/README.md
  - labdb_upload.py 섹션 신규 — 사용법 / 폴더 구조 / 재실행 안전성 /
    buildPayload 로직 / 활용 예 정리

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 11:25:26 +09:00
parent b822b49fc6
commit 60b630d3de
5 changed files with 367 additions and 32 deletions
+39 -26
View File
@@ -74,40 +74,53 @@ This step ensures the sensor is positioned correctly over your bladder for accur
- Apply ultrasound gel between the sensor and your skin
- Tap the **Start Alignment** button (large orange button)
### Step 1/3: Vertical Alignment
### 알고리즘 V1 (일반 사용자, 3-stage) / V2 (Clinical Alignment 세션, 6-stage)
The app will begin scanning and provide direction:
- **"Slide up ↑"** — slide the device upward toward your navel
- **"Slide down ↓"** — slide the device downward toward your pubic bone
- **"Slide up slightly ↑"** / **"Slide down slightly ↓"** — make small adjustments
- Move the sensor **slowly, 1-2mm at a time**
- When the vertical position is correct, the app will show **Step 2/3**
일반 진입 (모니터링 → Settings → 재측정 등) 은 **V1 (3-stage)**, Clinical
Home 의 **Sensor Alignment** 버튼으로 진입한 세션은 자동으로 **V2 (6-stage)**
로 활성화됩니다. Step indicator ("단계 N/M") 로 구분 가능.
> The hint text will show animated dots (e.g., "Slide up ↑.", "Slide up ↑..", "Slide up ↑...") to indicate the system is actively scanning.
### V1: 3-stage (일반 진입)
### Step 2/3: Lateral Alignment
**Step 1/3: Vertical Alignment**
- **"Slide up ↑"** / **"Slide down ↓"** / **"Slide up slightly ↑"** 등의 방향 안내
- 1~2mm 씩 천천히 이동, 위치가 잡히면 Step 2/3
1. The app will first show **"Checking..."** for a few seconds
2. Then it will guide you:
- **"Slide left ←"** — slide the device to the left
- **"Slide right →"** — slide the device to the right
3. Adjust until both lateral sensors (CH4 and CH5) detect the bladder equally
4. When balanced, the app will show **Step 3/3**
**Step 2/3: Lateral Alignment**
- **"Checking..."** → **"Slide left ←"** / **"Slide right →"** — CH4/CH5 균형
- 균형 잡히면 Step 3/3
### Step 3/3: Final Check (Green Zone)
**Step 3/3: Final Check (Green Zone)**
- CV + LR deviation 확인 → **"최적의 위치입니다!"** (2026-07-07 문구 개선)
- 배경 초록 + **5초 hold** (2026-07-07: 7초 → 5초 유지, 조기 unlock 방지 강화)
- **Start Scanning** 활성 → 측정 화면으로
1. The app checks overall alignment quality (CV + LR deviation)
2. If everything is optimal, the hint will show **"In position!"**
3. The screen background turns green and holds this state for **7 seconds** to confirm stability
4. The **Start Scanning** button (green) becomes active
5. Tap **Start Scanning** to proceed to the measurement screen
### V2: 6-stage (Clinical Alignment 세션 전용)
방광 하부/후벽 각도 오차를 실시간 보정하는 정밀 정렬 알고리즘.
| 단계 | 화면 문구 예 | 조건 |
|:---:|---|---|
| 1/6 INITIAL_ACCUM | "잠시 그대로 두세요 (n/10)" | baseline 10 cycle 누적 |
| 2/6 VERTICAL_CLIMB | "↑ 위로 조금씩 올리세요 (hit N/6)" | CH3 sliding window majority — 최근 6 프레임 중 3회 hit |
| 3/6 CH3_STABILIZE | "✓ ch3 검출 — 위치 유지 (n/10)" | CH3 안정 확인, 단발 flick 무시 (window=4, majority=3) |
| 4/6 CENTER_OPTIMIZE | "↑/↓ 조금 (CV=0.xx)" | CH0~CH3 chord CV 임계 0.15 |
| 5/6 LR_BALANCE | "← / → \|Δ\|=n" | CH4/CH5 균형 |
| 6/6 FINAL_CONFIRM | "✓ 최적의 위치입니다!" | 5 프레임 재확인 통과 → GREEN |
**추가 안전장치**:
- **Relaxed mode**: VERTICAL_CLIMB 에 20 프레임 (~5초) 이상 갇히면 CH3 없이도
상단 3채널 (CH0~CH2) 로 CENTER_OPT 진입. CH3 flicker 심한 자세에서 무한 대기 방지.
- **Soft hint**: 12 프레임 (~3초) 이상 반복되면 "↑ 위로" → "CH3 신호 확인 중 · 위치 유지"
로 문구 자동 완화.
- **6단계 완료 후 GREEN 5초 hold**: 사용자가 "측정 시작" 버튼 누를 시간 확보.
### If Alignment is Lost
- If the sensor shifts significantly after reaching "In position!":
- The app allows up to **3 consecutive failures** before exiting GREEN
- After 3 failures: **"Position lost. Restart above pubic bone."**
- The scanning will stop automatically
- Re-place the sensor and tap **Start Alignment** again
- Green Zone 유지 중 실패해도 즉시 리셋 X — **10 consecutive failures** (약 5초+)
까지 관대하게 유지 (2026-07-07 완화, 이전 3-strike → 10-strike). 이 사이는
GREEN 유지 + "최적의 위치입니다!" 표시.
- 10회 연속 실패: **"위치를 놓쳤습니다. 치골 위에서 다시 시작해주세요"** →
자동 재시작 대기.
### If the Sensor is Detached
- If the sensor loses contact with skin:
+67 -2
View File
@@ -2,8 +2,73 @@
## 작성일: 2026-04-23
## 작성자: dwjang
## 마지막 업데이트: 2026-05-26 (v6) — 1.0.0-design (versionCode 24)
## 권장 펌웨어: **VBTFW0116** (pending slot 1→8 확장으로 ADC drop 안정)
## 마지막 업데이트: 2026-07-10 (v7) — 1.2.0-demo (versionCode 26)
## 권장 펌웨어: **VBTFW0120+** (mim FIFO 지원 필수. VBTFW0121 은 `mls mode 0` handler 취약)
### v7 주요 변경 요약 (2026-07-06 ~ 2026-07-10)
**BLE / Firmware 대응**
- **`msp` 완전 제거 → `mim` (FIFO 15 sample) 로 통일** (커밋 34ae4d7/deae8a7/9907931).
신 firmware 는 mim 만 사용. `rsp:` 파서는 legacy 응답 대비 유지.
- **mtb queue overrun freeze 3-layer fix** (커밋 9907931). 실측 로그에서 mtb 응답
stream 중 `msn`/`mim` 이 끼어들어 firmware GATT queue 꼬임 → `mls mode 0`
에서 완전 freeze 확인. 상세: [BLE_PROTOCOL_REFERENCE.md](docs/BLE_PROTOCOL_REFERENCE.md) §2.6.
- `isMtbBusy` 프로퍼티 + battery/mim polling 시 skip
- mtb 3초 timeout + `consecutiveMtbTimeouts` state
- 3회 연속 실패 시 자동 `forceDisconnectAndReconnect()`
- **Watchdog timeout 15초 → 25초 연장** (커밋 0383a21). 재연결 후 첫 RX 가 14초
지연 도착하는 케이스 허용.
- **좀비 세션 방지**: `onConnectionStateChanged` 콜백을 CCCD write 완료 시점으로
이동 (BleManager). `fwFallbackTimer` / `cccdRetryTimer` / `mtbTimeoutRunnable`
disconnect 시 취소.
- **Auto scan 명령 `maa` → `mtb` 로 전환** (커밋 29b6ca5). maa 는 IMU 응답
없음 (posture 갱신 X). mtb 는 reb×6 + raa + rim 이라 auto scan 중에도 posture
실시간 업데이트.
- **lastBleRxAt 갱신 확대** (커밋 3a41864). `processReceivedData` 진입점에서
모든 유효 응답에 대해 갱신 → "기기 응답 없음" false-positive 배너 해소.
**Alignment (V2 6-stage)**
- **CH3 flicker 무한 대기 방지** (커밋 e808b5b). VERTICAL_CLIMB 진입 조건을
"3연속 hit" → "최근 6프레임 중 3회 hit" sliding window majority 로 완화.
20 프레임 대기 시 상단 3채널 relaxed mode 진입 (CH3 없이도 정렬 완료).
12 프레임 반복 시 문구 자동 완화 ("↑ 위로" → "CH3 확인 중").
- **6단계 완료 후 "측정 시작" 버튼 활성화 fix** (커밋 49aab7b). 기존
`phase == LR_BALANCE` 조건이 실제 완료 phase (`FINAL_CONFIRM`) 를 못 잡아
버튼 disable 되던 버그. `state=="commit" && phase==FINAL_CONFIRM` 로 수정.
- **GREEN 진입 후 5초 hold** (커밋 159116d). V2 에도 V1 처럼 hold 로직 추가.
순간 flick 으로 GREEN 즉시 풀리는 UX 문제 해소.
- **GREEN zone 조기 리셋 완화** (커밋 7484077). V1 의 3-strike → **10-strike**
(약 5초+). 미세 자세 흔들림으로 "0/3 정렬시작" 리셋되던 버그 완화.
- **문구 개선**: "제 위치입니다!" → **"최적의 위치입니다!"** / "In position!"
→ "Optimal position!" (커밋 7484077).
**UI / UX**
- **Posture 라벨**: "서있음" → **"일어남"** (`piezo_posture_upright`, 한글).
영문 "Upright" 유지.
- **도넛차트 posture chip stuck 버그** (커밋 149e511/3abc024): `imuCollector.onComplete`
콜백이 DisposableEffect race 로 즉시 null 되던 문제. 세 화면
(PiezoMonitoring / PlacementGuide / ClinicalLive) 의 DisposableEffect 에서
콜백 null 정리 제거. stale 콜백은 다음 화면 진입 시 자기 콜백으로 덮어씀.
**Clinical 세션 / labdb**
- **labdb 자동 업로드 강화** (커밋 db6e7e1/93b8650/8c17407):
- `endMeasurement` 자동 업로드 조건 완화 (`isRegistered` 만 체크, `lastStatus == "active"` 조건 제거)
- 신규 `LabdbAutoRetry` object — ClinicalHome 진입 시 미업로드 폴더 자동 재시도
- 상단 배너 UI (진행 중 / 완료 결과)
- **KnownDeviceStore clinical flow 제한 제거** (커밋 b822b49). 일반 모드에서도
연결한 기기 자동 저장 + DeviceScan 상단에 표시.
**Python tools**
- **`tools/labdb_upload.py` 신규** — LabdbUploader.kt 의 `buildPayload` 로직을
Python 으로 이식. 외부 저장 세션 폴더 일괄 업로드용.
자세한 사용법: [tools/README.md](tools/README.md).
**참고**
- 이번 세션에서 발견되었으나 아직 미조치: PiezoMonitoringView (2900L) /
PlacementGuideView (2200L) 대형 파일 분해, Design Token 중앙화, ViewModel
도입 등 아키텍처 개선 사항은 별도 로드맵으로 진행 예정.
### v6 주요 변경 요약 (2026-05-12 ~ 2026-05-26)
### v6 주요 변경 요약 (2026-05-12 ~ 2026-05-26)
+44 -4
View File
@@ -96,10 +96,49 @@ VesiScan Basic Android 앱이 VBT 디바이스(VBTFW0116+ 펌웨어)와 주고
- 측정 중이었으면 측정 일시정지, 복구 시 사용자가 다시 시작
### 2.5 Watchdog (좀비 감지)
파일: [`BleManager.kt:528+` watchdogJob](../app/src/main/java/com/example/medilightv2android/ble/BleManager.kt#L528)
파일: [`BleManager.kt` watchdogJob](../app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt)
- 별도 스레드로 30초 주기 RX 마지막 수신 시간 확인
- N초 이상 무응답 + isConnected.value = true 라면 GATT 강제 reset → 재연결
- 별도 스레드가 5초 tick 으로 RX 마지막 수신 시각 확인.
- **2026-07-07**: watchdog timeout **15 → 25초** 연장. 실측 로그에서 재연결
후 첫 RX 가 14초 지연 후 도착하는 케이스 확인 — peripheral / OS BLE
스택의 좀비 회복 시간 허용. 15초로는 회복 직전에 forced reconnect 발동
→ 무한 재연결 루프 문제.
- Silence 10초~timeout 사이면 **heartbeat 발동**: 2026-07-08 부터 `msp` 대신
`sendImuFifoQuery()` (mim). `isMtbBusy` 시 skip.
- 정리는 UI thread 로 위임 (`handler.post`) — watchdog thread 에서 직접
GATT close 하면 UI thread 의 sendRaw 와 race.
### 2.6 ⚠ 알려진 firmware freeze (VBTFW0121) — mtb queue overrun
**증상**: `mtb` 응답 stream (reb×6 + raa + rim) 진행 중에 다른 명령
(`msn`, `mim`) 이 TX 로 끼어들면 firmware GATT queue 가 꼬여 이후 명령
응답이 실종. 특히 그 상태에서 `mls mode 0` 이 결정타가 되어 BLE
advertising 까지 죽는 **완전 freeze** 실측 확인 (2026-07-07 11:26 세션 로그).
**앱 측 3-layer 회피** (2026-07-08 커밋 9907931):
**Layer 1 — BleManager gating (`isMtbBusy`)**
```kotlin
val isMtbBusy: Boolean
get() = piezoCollector.isMultiChannel && !piezoCollector.isComplete
```
- `batteryTimer` / `batteryRetryTimer`: `sendBatteryQuery` 앞에 skip
- Watchdog silence heartbeat: `sendImuFifoQuery` 앞에 skip
- 상위 (View 레이어) 도 `mim` 폴링 앞에서 `isMtbBusy` 체크
**Layer 2 — mtb 3초 timeout**
- `sendMtb()` 후 3초 timeout runnable, `raa` 응답 오면 취소.
- Timeout 시 `consecutiveMtbTimeouts` 증가 + `piezoCollector.reset()` /
`imuCollector.reset()` 해제.
**Layer 3 — UI 안내 + 자동 재연결**
- `PlacementGuideView` 가 `consecutiveMtbTimeouts` 관찰.
- 1~2회: `"기기 응답 지연"` 배너
- **3회 연속: `forceDisconnectAndReconnect()` 자동 호출** — 사용자가 앱을
방치해도 자동 복구.
**Firmware 팀 리포트 대상**: VBTFW0121 의 `mls mode 0` handler 가 큐
스트레스 상태에서 취약. 앱 fix 로 회피는 되지만 근본은 firmware.
---
@@ -164,7 +203,8 @@ fun verify(data: ByteArray): Boolean
| `msn?` | BE [0] | `sendBatteryQuery()` ([L463](../app/src/main/java/com/example/medilightv2android/ble/BleManager.kt#L463)) | `rsn:` | 배터리 mV 조회 |
| `mid?` | ASCII " " | `sendDeviceInfoQuery()` ([L467](../app/src/main/java/com/example/medilightv2android/ble/BleManager.kt#L467)) | `rid:` | FW/HW/Serial 조회 |
| `mls?` | BE [state] | `sendLedMode(state)` ([L471](../app/src/main/java/com/example/medilightv2android/ble/BleManager.kt#L471)) | `rls:` | LED 모드 변경 |
| `msp?` | ASCII " " | `sendImuQuery()` ([L475](../app/src/main/java/com/example/medilightv2android/ble/BleManager.kt#L475)) | `rsp:` | IMU 단일 샘플 조회 |
| ~~`msp?`~~ | ~~ASCII " "~~ | ~~`sendImuQuery()`~~ | `rsp:` | ⚠ **2026-07-08 완전 제거** — 신 firmware 는 `mim` 만 사용. `rsp:` 파서는 legacy 응답 대비 유지. |
| `mim?` | ASCII " " | `sendImuFifoQuery()` | `rim:` (15 sample) | ★ IMU FIFO — piezo 무음, walking detector 시계열 확보용. `imuCollector.reset()` 을 먼저 호출하므로 mtb 진행 중 (isMtbBusy=true) 이면 caller 가 반드시 skip 해야 함 (mtb 의 rim 파괴 방지). |
### 4.1 maa / mtb / maa 송신 throttle (canSendMaa)
+122
View File
@@ -1512,6 +1512,128 @@ async function upload(key, session) {
\## 12b. 앱 측 Auto Retry Policy (Android app, 2026-07-09)
Android 앱 (VesiScan-Basic demo-final / tab-navigation / cloud-mvp) 은 매
session 마다 사용자가 Upload 버튼을 누르지 않아도 되도록 **자동 업로드 +
재시도 큐** 를 내장합니다.
\### 12b.1 endMeasurement 자동 업로드
Clinical 세션 종료 시 (`ClinicalSessionStore.endMeasurement()`) `apiKey` 가
등록되어 있으면 (`LabdbCredentials.isRegistered`) 무조건 `LabdbUploader.
uploadAsync(context, folder)` 를 호출. **fire-and-forget** 이므로 응답 대기
없이 다음 화면으로 진행.
\- 이전 정책: `lastStatus == "active"` 조건 필요 → 상태 갱신 안 된 첫 진입 시
자동 업로드 skip 되던 문제.
\- 2026-07-09 이후: `isRegistered` 만 체크. 서버가 401/403 을 반환하면 상위
로직에서 status 재조회.
\### 12b.2 LabdbAutoRetry object (`app/src/main/java/.../labdb/LabdbAutoRetry.kt`)
Clinical Home 진입 시 (`LaunchedEffect(Unit)`) `retryPending(context)` 자동
호출. 로직:
```
1. LabdbCredentials.isRegistered 확인 → 없으면 no-op
2. ClinicalSessionStore.recentFinalizedFolders (tab-nav/cloud-mvp) 또는
listOfNotNull(lastFinalizedFolder) (demo-final) 스캔
3. labdb_upload.json 마커 없는 폴더만 pending
4. 순차 (병렬 X) LabdbUploader.uploadBlocking 실행
5. 결과: labdb_upload.json (성공) / labdb_upload_error.json (실패)
```
\### 12b.3 UI 배너
Clinical Home 상단:
\- 진행 중 (`pendingCount > 0` 또는 `uploadingName != null`):
파란 배너 "자동 업로드 진행 중 · 남은 세션 N · <folderName>"
\- 완료 (`lastResultMessage != null`):
초록/주황 배너 "N uploaded, M failed" + 확인 버튼
\### 12b.4 State 노출
```kotlin
object LabdbAutoRetry {
val pendingCount: MutableIntState // UI 배너 카운트
val uploadingName: MutableState<String?> // 현재 업로드 중 폴더
val lastResultMessage: MutableState<String?> // 완료 후 결과 요약
fun retryPending(context: Context) // 트리거
fun dismissResult() // 배너 확인
}
```
\### 12b.5 재시도 안전성
\- 이미 업로드된 폴더 (`labdb_upload.json` 있음) 는 skip
\- 실패 폴더는 `labdb_upload_error.json` 만 남기고 다음 진입 시 재시도
\- 네트워크 순간 이슈 → 다음 진입에서 자동 복구
\- 앱 종료 후 재실행 → `ensureHistoryLoaded` 로 recent 폴더 복원 → 재시도
\---
\## 13. 지원
+95
View File
@@ -75,3 +75,98 @@ scan_id, timestamp, ..., volume_ml, lr_ratio, ..., channel, s0..s99
2. **알고리즘 변경 검증**: 새 fix 후 같은 CSV 로 `--ablation` 돌려 회귀 여부 확인
3. **Device-to-device variance**: 두 device 의 같은 phantom 측정 → CV 비교
4. **lr_ratio 패턴**: human cohort 의 `--plot` 으로 lr 분포 시각화
---
## labdb_upload.py — 세션 폴더 일괄 업로드
앱이 저장한 세션 폴더 (`measurement_<name>.json` 포함) 를 labdb REST API
로 일괄 업로드. 앱 안의 `LabdbUploader.kt` (buildPayload) 로직을 그대로
Python 으로 옮긴 것으로, 폰이 없는 상황에서 데스크톱에 백업한 데이터를
바로 서버로 넣을 때 사용.
### Setup
```bash
# 별도 의존성 없음 (표준 라이브러리만 사용)
export LABDB_API_KEY=xbk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
apiKey 는 labdb admin 콘솔 또는 backend 담당자로부터 발급.
### Usage
```bash
# 기본 — 폴더 안의 모든 세션 업로드
python tools/labdb_upload.py "C:/Users/장동우/Desktop/newdata"
# apiKey 를 명령줄로 전달
python tools/labdb_upload.py ./sessions --api-key xbk_live_xxx...
# dry-run — 전송하지 않고 payload 검증만
python tools/labdb_upload.py ./sessions --dry-run
# 이미 업로드된 폴더도 재전송 (labdb_upload.json 마커 무시)
python tools/labdb_upload.py ./sessions --force
# meta.data_type 없을 때 사용할 fallback (기본 "001")
python tools/labdb_upload.py ./sessions --data-type 002
```
### 폴더 구조 요구사항
각 세션 폴더는 앱이 만든 표준 형식:
```
<folder>/
meta_<folder>.json
measurement_<folder>.json ← 이걸 payload 로 변환
adc.csv
imu.csv
ble.log
labdb_upload.json ← 성공 시 생성 (sessionId, inserted 개수 등)
labdb_upload_error.json ← 실패 시 생성 (다음 실행 시 참고)
```
`measurement.json` 또는 `measurement_<folder>.json` 이 있으면 대상. 없으면
"measurement json not found" 로 실패.
### 재실행 안전성
- 이미 업로드된 폴더 (`labdb_upload.json` 있음) 는 자동 skip
- `--force` 로 강제 재전송 가능
- 실패한 폴더는 다음 실행 시 자동 재시도
### 출력 예시
```
[OK ] HUMAN-kai_VBT26040302_SITTING_ALIGN_0CM_2026-07-08_110903: uploaded: 130/130 (dup=0)
[OK ] HUMAN-kai_VBT26040302_SITTING_ALIGN_1CM_2026-07-08_111049: uploaded: 3/3 (dup=0)
[FAIL] HUMAN-kai_VBT26050202_SITTING_ALIGN_2CM_2026-07-08_161928: upload fail: HTTP 401
[OK ] ... (skip already uploaded)
summary: 19 uploaded, 0 skipped, 1 failed (total 20)
```
### buildPayload 로직 (LabdbUploader.kt 와 동일)
- **testId**: 폴더명 sanitize (`^[A-Za-z0-9_-]{1,40}$`). 40자 초과 시 앞
31자 + `_` + 8자 SHA-1 prefix 로 축약 (uniqueness 유지).
- **dataType**: `meta.data_type` 우선, 없으면 `--data-type` fallback.
- **records[]**: `measurement.json` 의 각 cycle → labdb record 로 변환:
- `sensor.imu`: 마지막 (가장 최근) IMU sample
- `sensor.imu_samples`: 전체 시계열
- `channels[0..5]`: peak / peakIdx / data 배열
- alignment session 은 align_phase / align_score / align_hint 등 부가 필드.
- **params**: posture / step / is_alignment / examiner / firmware / hardware / subject 등
meta 요약.
### 활용 예
1. **개발용 백업 재전송**: 폰이 없는 상황에서 데스크톱에 백업한 세션 폴더를
labdb 로 일괄 업로드.
2. **자동 재시도 실패 복구**: 앱의 `LabdbAutoRetry` 도 실패한 케이스 (예:
apiKey 만료) → apiKey 갱신 후 스크립트로 일괄 재전송.
3. **다른 폰 → labdb 이관**: A 폰에서 측정 → `Downloads/VesiScan_Sessions/`
폴더 → PC 로 복사 → 스크립트로 서버 반영.