# Rep Detection & Segmentation Research

## Overview

Roomfit의 운동 데이터(position, speed, accel, fLoad, icmd)를 기반으로
Rep Count 및 Rep Segmentation 기능을 구현하기 위한 조사 결과.

VBT(Velocity-Based Training) 업계의 검증된 방법론을 기반으로 한다.

---

## 1. 산업계 현황: 인코더 기반 VBT 장비

### 1.1 주요 상용 장비

| 장비 | 센서 | 샘플링 | Rep 감지 방식 |
|------|------|--------|---------------|
| **GymAware** | 로터리 인코더 + 리트랙터블 케이블 | 50-100Hz | displacement threshold + velocity sign change |
| **Tendo Unit** | 리니어 포지션 트랜스듀서 | 50Hz | velocity threshold (>0.1 m/s) + 원복 감지 |
| **OpenBarbell V3** | 로터리 인코더 + 리트랙터블 케이블 | ~50Hz | min displacement + velocity sign + rest timeout |
| **RepOne** | 로터리 인코더 | 50-100Hz | GymAware 기반, 오픈소스 유래 |

**핵심**: 모든 상용 장비가 동일한 원리 — position에서 peak/valley 감지, velocity 부호로 phase 구분.

### 1.2 OpenBarbell V3 (핵심 참조)

하드웨어 구성이 Roomfit과 거의 동일 (로터리 인코더 + 리트랙터블 케이블).
오픈소스로 알고리즘이 공개되어 있다.

**GitHub**: `squatsandscience/OpenBarbell-V3`

**Rep 감지 알고리즘 요약**:
1. 3-5 sample 이동평균으로 position 스무딩
2. velocity = delta_position / delta_time
3. Rep 시작: velocity가 상방 threshold 초과
4. Rep 종료: position이 시작점의 "end zone" 이내로 복귀
5. 최소 rep time 체크 (< 0.3s이면 노이즈로 판정)
6. 최소 displacement 체크 (설정 가능)

### 1.3 Roomfit의 고유 장점

| 데이터 | 일반 VBT 장비 | Roomfit |
|--------|--------------|---------|
| position | O (직접 측정) | O |
| speed | X (position 미분 필요) | O (MCU가 50ms 주기로 계산) |
| accel | X (2차 미분 필요) | O (MCU 제공) |
| force/load | X (추정만 가능) | O (fLoad: icmd 역산, icmd: 직접 측정) |
| 저항 모드 | 없음 | O (weightMode: constant/negative/squeeze) |
| 포지션 구간 | 없음 | O (region: MCU 판정) |
| 좌우 독립 | 없음 | O (L/R 별도 모터) |

일반 VBT 장비가 position 하나로 모든 것을 유도하는 반면,
Roomfit은 MCU가 이미 계산한 speed, accel, fLoad를 직접 수신한다.
**position 미분에 따른 노이즈 문제가 원천적으로 없다.**

---

## 2. Rep의 물리적 정의

### 2.1 1 Rep의 구조

```
1 Rep = Concentric Phase + Eccentric Phase

Concentric (구심 수축): 근육이 수축하며 케이블을 당김
  → position 증가, speed > 0

Eccentric (원심 수축): 근육이 이완하며 케이블을 놓음
  → position 감소, speed < 0
```

### 2.2 Position 파형

```
position (mm)
  ^
  |    /\        /\        /\
  |   /  \      /  \      /  \
  |  /    \    /    \    /    \
  | /      \  /      \  /      \
  |/        \/        \/        \
  +-----------------------------------> time
  | rep 1   | rep 2   | rep 3   |
  
  valley    peak    valley    peak    valley
  (rest)    (top)   (rest)    (top)   (rest)
```

### 2.3 Phase 상세 모델

NSCA 표준 (Haff & Triplett, 2015 — "Essentials of Strength Training and Conditioning"):

```
1. CONCENTRIC phase: 근육 수축, 케이블 당김 (speed > 0)
2. TOP TURNAROUND: 최대 수축 지점, 잠시 정지 (speed ≈ 0)
3. ECCENTRIC phase: 근육 이완, 케이블 놓음 (speed < 0)
4. BOTTOM TURNAROUND: 최저점/휴식, 다음 rep 대기 (speed ≈ 0)
```

### 2.4 케이블 머신의 특수성

**케이블 머신은 바벨보다 rep 감지가 쉽다:**
- 명확한 rest position 존재 (케이블 완전 원복)
- 인코더가 케이블 변위를 직접 측정 (수직 투영 불필요)
- position 신호가 깨끗함 (바벨 흔들림/요동 없음)
- 매 rep마다 "시작점 복귀" 패턴이 명확함

---

## 3. Rep 감지 알고리즘

### 3.1 감지 전략 비교

| 접근법 | 원리 | 장점 | 단점 |
|--------|------|------|------|
| Speed 부호 전환 | speed 0-crossing 감지 | 직관적, MCU가 이미 계산 | turnaround에서 진동 → 오탐 |
| Position peak/valley | hysteresis로 극값 감지 | 가장 안정적 | threshold 튜닝 필요 |
| Schmitt trigger on velocity | 이중 threshold | 전환 구간 명확 | 파라미터 2개 |
| **Position + Speed 결합** | position 극값 + speed 부호 교차 검증 | **가장 robust** | 구현 약간 복잡 |

### 3.2 Hysteresis 기반 Peak/Valley 감지

VBT 문헌에서 가장 권장되는 방법.

**원리**: 단순 극값이 아닌, 충분한 변위 반전이 있을 때만 peak/valley로 인정.

```
Hysteresis band H = 10-30mm (운동 ROM에 비례하여 조정)

Peak 확정 조건: position이 local maximum에서 H 이상 하강
Valley 확정 조건: position이 local minimum에서 H 이상 상승
```

**scipy.signal.find_peaks의 `prominence` 파라미터와 동일한 개념.**

### 3.3 Schmitt Trigger on Velocity

velocity에 이중 threshold를 적용하는 방법.

```
Concentric 시작: speed가 +V_threshold 상향 돌파 (예: +20 mm/s)
Eccentric 시작:  speed가 -V_threshold 하향 돌파 (예: -20 mm/s)
Dead band:       |speed| < V_threshold → 이전 phase 유지
```

참고: Jidovtseff et al. (2011)에서 기술.

### 3.4 권장 알고리즘: 6-State FSM

상용 VBT 장비와 학술 문헌에서 검증된 접근법.

```
       ┌─────────┐
       │  IDLE    │ ← 초기 상태 / 세트 완료 후
       └────┬─────┘
            │ position > start + min_displacement AND speed > speed_threshold
            v
       ┌─────────────┐
       │ CONCENTRIC   │ ← 케이블 당기는 중 (speed > 0)
       └────┬─────────┘
            │ speed < +deadband (속도 감소, turnaround 진입)
            v
       ┌─────────────┐
       │ PEAK_HOLD    │ ← 최대 수축 지점 (speed ≈ 0)
       └────┬────┬────┘
            │    │ dwell > set_rest_timeout → SET_COMPLETE
            │    v
            │ speed < -deadband (하강 시작)
            v
       ┌─────────────┐
       │ ECCENTRIC    │ ← 케이블 놓는 중 (speed < 0)
       └────┬─────────┘
            │ speed > -deadband AND position ≈ start position
            v
       ┌─────────────┐
       │ VALLEY_HOLD  │ ← 바닥, rep 1개 완성! → REP COUNT++
       └────┬────┬────┘
            │    │ dwell > set_rest_timeout → SET_COMPLETE
            │    v
            │ speed > +deadband (다음 rep 시작)
            v
       ┌─────────────┐
       │ CONCENTRIC   │ (반복)
       └──────────────┘
```

**Rep은 ECCENTRIC 완료 시점(VALLEY_HOLD 진입)에 카운트한다.**

### 3.5 상태 전이 상세

```
IDLE → CONCENTRIC:
  guard: displacement > start_threshold AND speed > min_speed
  action: rep_start_time 기록, rep_start_position 기록

CONCENTRIC → PEAK_HOLD:
  guard: speed < +speed_deadband (debounce_time 이상 지속)
  action: peak_position 기록, concentric 메트릭 계산

PEAK_HOLD → ECCENTRIC:
  guard: speed < -speed_deadband
  action: turnaround_duration 기록

PEAK_HOLD → SET_COMPLETE:
  guard: dwell_time > set_rest_timeout (3초 이상 정지)
  action: 세트 메트릭 최종 집계

ECCENTRIC → VALLEY_HOLD:
  guard: speed > -speed_deadband AND position < valley_threshold
  action: eccentric 메트릭 계산, REP COUNT INCREMENT

VALLEY_HOLD → CONCENTRIC:
  guard: speed > +speed_deadband
  action: inter_rep_rest_time 기록, 새 rep 시작

VALLEY_HOLD → SET_COMPLETE:
  guard: dwell_time > set_rest_timeout
  action: 세트 메트릭 최종 집계
```

### 3.6 파라미터 (문헌 기반 시작값)

| 파라미터 | 값 | 단위 | 근거 |
|---------|-----|------|------|
| `min_displacement` | 50 | mm | 최소 ROM. 이 이하 움직임은 rep 아님 |
| `speed_deadband` | ±20 | mm/s | turnaround 판정 구간. 20Hz에서 position noise ±1mm → 최소 감지 velocity ≈ 20mm/s |
| `debounce_time` | 100 | ms | 2 samples @ 20Hz. 순간 진동 필터 |
| `set_rest_timeout` | 3000 | ms | 세트 종료 판정. 60 samples |
| `min_rep_duration` | 400 | ms | 8 samples. 이 이하는 노이즈 |
| `max_rep_duration` | 15000 | ms | 안전 타임아웃. 극도로 느린 eccentric도 커버 |
| `hysteresis_band` | 20 | mm | position peak/valley 확정용 |

**이 값들은 시작점이며, 실제 기구에서 수집한 데이터로 튜닝해야 한다.**

### 3.7 20Hz 샘플링 고려사항

```
50ms 간격 → 각 sample이 비교적 큰 시간 창

Position noise (로터리 인코더): 일반적으로 ±0.5-2mm
Velocity (1차 차분): (pos[n] - pos[n-1]) / 0.05
  → noise ±1mm 가정 시, 최소 감지 velocity ≈ ±20 mm/s

MCU가 speed를 직접 제공하므로:
  - position 미분 불필요
  - MCU 레벨에서 이미 필터링됨
  - 추가 스무딩 불필요할 가능성 높음 (실측 후 판단)
```

---

## 4. Per-Rep 메트릭

### 4.1 VBT 표준 메트릭

Rep 완료 시점(ECCENTRIC → VALLEY_HOLD)에 아래 메트릭을 계산한다.

#### A. Range of Motion (ROM)

```
rom_mm = peak_position - valley_position
concentric_rom_mm = ROM of concentric phase
eccentric_rom_mm = ROM of eccentric phase
(정상 rep이면 concentric_rom ≈ eccentric_rom)
```

#### B. Velocity

```
mean_concentric_velocity  = avg(speed samples during concentric)     [mm/s]
peak_concentric_velocity  = max(speed samples during concentric)     [mm/s]
mean_eccentric_velocity   = avg(|speed| samples during eccentric)    [mm/s]
peak_eccentric_velocity   = max(|speed| samples during eccentric)    [mm/s]
```

**Mean Concentric Velocity (MCV)는 VBT의 핵심 지표.**
Gonzalez-Badillo & Sanchez-Medina (2010): MCV와 %1RM은 거의 완벽한 선형관계.

#### C. Force

```
mean_concentric_force = avg(fLoad during concentric)    [kg]
peak_concentric_force = max(fLoad during concentric)    [kg]
mean_eccentric_force  = avg(fLoad during eccentric)     [kg]
peak_eccentric_force  = max(fLoad during eccentric)     [kg]
```

Roomfit 고유 데이터로 icmd(mA)도 함께 기록:
```
mean_concentric_icmd = avg(icmd during concentric)      [mA]
peak_concentric_icmd = max(icmd during concentric)      [mA]
```
icmd는 모터가 실제로 출력한 토크. fLoad는 icmd의 파생값.

#### D. Power

```
instantaneous_power = fLoad(kg) * 9.81(m/s^2) * speed(m/s)
                    = fLoad * 0.00981 * speed_mm_s    [W]

mean_concentric_power = avg(instantaneous_power during concentric)   [W]
peak_concentric_power = max(instantaneous_power during concentric)   [W]
```

#### E. Time

```
rep_duration_ms      = valley_to_valley time                [ms]
concentric_time_ms   = valley_to_peak time                  [ms]
eccentric_time_ms    = peak_to_valley time                  [ms]
turnaround_time_ms   = time in PEAK_HOLD state              [ms]
conc_ecc_ratio       = concentric_time / eccentric_time

일반적 비율:
  - 1:2 → 제어된 동작 (일반 트레이닝)
  - 1:1 → 폭발적 동작
  - 1:3+ → 템포 트레이닝
```

#### F. Time Under Tension (TUT)

```
Per rep: rep_duration_ms
Per set: sum(all rep durations)

Load-weighted TUT (더 정밀):
  tut_weighted = sum(fLoad[i] * delta_t) for all samples in rep
  → 높은 부하 = 더 큰 tension으로 가중
```

### 4.2 고급 메트릭

#### Velocity Loss (피로 지표)

```
velocity_loss_pct = (MCV_rep_n - MCV_rep_1) / MCV_rep_1 * 100

Sanchez-Medina & Gonzalez-Badillo (2011):
  - velocity loss > 20-40%이면 세트 종료 권장
  - 근비대: 20-30% loss 허용
  - 파워: 10-15% loss에서 종료
```

#### Rate of Force Development (RFD)

```
rfd = delta_force / delta_time during initial concentric phase
  → concentric 시작 후 첫 50-100ms 구간에서 계산
```

#### Work (일)

```
work = sum(force[i] * delta_position[i]) over phase    [J]
     ≈ mean_force * ROM (constant mode에서)
```

#### Impulse (충격량)

```
impulse = sum(force[i] * delta_t) over phase    [N·s]
```

### 4.3 Roomfit 고유 메트릭

일반 VBT 장비에서는 불가능하지만 Roomfit은 가능한 것들:

```
weight_mode_during_rep: constant(0) / negative(1) / squeeze(2)
  → 모드별 rep 프로파일 분석 가능

region_transitions: rep 중 region 변화 기록
  → 구간별 저항 변화 분석

icmd_vs_fload_error: icmd와 fLoad 간 차이
  → 제어 오차 모니터링

lr_imbalance: L/R 간 ROM, MCV, peak force 차이
  → 근력 비대칭 감지
```

---

## 5. 좌/우 독립 처리

### 5.1 원칙

모든 물리량이 L/R 독립이므로, Rep Detector도 L/R 독립으로 동작한다.

```
RepDetector(left)  ← positionL, speedL, accelL, fLoadL, icmdL
RepDetector(right) ← positionR, speedR, accelR, fLoadR, icmdR
```

각 detector가 독립적으로 FSM을 유지하고, 독립적으로 rep을 카운트한다.

### 5.2 L/R 비대칭 분석

양쪽 detector의 rep 메트릭을 비교하여 비대칭을 감지:

```
rom_imbalance     = |rom_L - rom_R| / max(rom_L, rom_R) * 100
velocity_imbalance = |mcv_L - mcv_R| / max(mcv_L, mcv_R) * 100
force_imbalance    = |peak_force_L - peak_force_R| / max(...) * 100
```

---

## 6. 학술 참고문헌

### 핵심 논문

1. **Gonzalez-Badillo, J.J. & Sanchez-Medina, L. (2010)**
   "Movement velocity as a measure of loading intensity in resistance training."
   *International Journal of Sports Medicine*, 31(5), 347-352.
   → VBT 기초 논문. MCV와 %1RM의 선형관계 확립. Rep segmentation 방법론 기술.

2. **Sanchez-Medina, L. & Gonzalez-Badillo, J.J. (2011)**
   "Velocity loss as an indicator of neuromuscular fatigue during resistance training."
   *Medicine & Science in Sports & Exercise*, 43(9), 1725-1734.
   → Rep별 velocity loss를 피로 지표로 사용. Rep-by-rep 분석 방법론.

3. **Jidovtseff, B., Harris, N.K., Crielaard, J.M., & Cronin, J.B. (2011)**
   "Using the loading-velocity relationship for 1RM prediction."
   *Journal of Strength and Conditioning Research*, 25(1), 267-270.
   → Velocity thresholding for rep segmentation 기술.

4. **Drinkwater, E.J., et al. (2007)**
   "Validation of an optical encoder during free weight resistance movements
   and analysis of bench press sticking point power during fatigue."
   *Journal of Strength and Conditioning Research*, 21(2), 510-517.
   → GymAware 인코더 방식 검증. Position → velocity 미분 및 rep segmentation.

5. **Jovanovic, M. & Flanagan, E.P. (2014)**
   "Researched applications of velocity based strength training."
   *Journal of Australian Strength & Conditioning*, 22(2), 58-69.
   → VBT 방법론 종합 리뷰. Rep detection 및 메트릭 알고리즘.

6. **Perez-Castilla, A., et al. (2019)**
   "Reliability and concurrent validity of seven commercially available devices
   for the assessment of movement velocity at different intensities during the bench press."
   *Journal of Strength and Conditioning Research*, 33(5), 1258-1265.
   → LPT 기반 VBT 장비 비교. Rep detection criteria.

### 케이블 머신 특화

7. **Frost, D.M., Cronin, J.B., & Newton, R.U. (2010)**
   "A biomechanical evaluation of resistance."
   *Sports Medicine*, 40(4), 303-326.
   → 케이블 기반 저항 운동의 force-position 관계.

### 교과서

8. **Haff, G.G. & Triplett, N.T. (2015)**
   "Essentials of Strength Training and Conditioning" (4th ed.), NSCA.
   → Rep phase model (concentric/turnaround/eccentric) 정의의 표준.

9. **Thompson, S.W., et al. (2020)**
   "The effectiveness of two methods of prescribing load on maximal strength development."
   *Sports Medicine*, 50, 919-938.
   → Velocity-based rep detection 및 load prescription 리뷰.

### 오픈소스

10. **OpenBarbell V3** — GitHub: `squatsandscience/OpenBarbell-V3`
    → 로터리 인코더 + 리트랙터블 케이블. Roomfit과 거의 동일한 하드웨어.
    → C/Arduino 펌웨어에 rep detection 알고리즘 공개.
    → React Native 컴패니언 앱에서 메트릭 계산 로직 확인 가능.

---

## 7. 구현 방향 요약

### 아키텍처 배치

```
packages/roomfit_protocol/        (기존, 변경 없음)
  ReportResponse → 50ms raw data

lib/core/device/                  (기존, 변경 없음)
  DeviceGateway → MotionState stream

lib/features/exercise/            (신규)
  domain/
    models/
      rep_phase.dart              ← enum: idle, concentric, peakHold, eccentric, valleyHold, setComplete
      rep_data.dart               ← 1개 rep의 전체 메트릭
      exercise_set.dart           ← rep 목록 + 세트 통계
    services/
      rep_detector.dart           ← 6-State FSM, L/R 독립 인스턴스
      rep_metrics_calculator.dart ← phase별 sample → 메트릭 계산
    providers/
      exercise_providers.dart     ← Riverpod provider (MotionState → RepDetector → UI)
  presentation/
    widgets/
      rep_counter_widget.dart
      rep_metrics_widget.dart
      set_summary_widget.dart
```

### TDD 순서

1. **RED**: RepDetector에 position/speed 시계열 입력 → rep count 검증
2. **GREEN**: 6-State FSM 최소 구현
3. **RED**: per-rep 메트릭(ROM, MCV, peak force 등) 검증
4. **GREEN**: RepMetricsCalculator 구현
5. **REFACTOR**: 파라미터 튜닝, 엣지 케이스 처리

### 핵심 설계 원칙

- RepDetector는 **순수 Dart**, Flutter 의존성 없음 (테스트 용이)
- MotionState sample을 하나씩 feed하는 **스트리밍 방식** (배치 아님)
- L/R 독립 인스턴스 (동일 클래스, 다른 입력)
- 파라미터는 외부 주입 가능 (운동 종류별 튜닝)

---

## 8. MCU 정합성 노트 (실제 펌웨어 소스 기반)

이 섹션은 `docs/reference/mcu-source/`의 WESPION 펌웨어 (STM32F446ZE,
v1.0.2 / v1.1.0-dev) 소스를 직접 읽고 발견한 주의사항이다. 합성
데이터로는 잡히지 않지만 실기기 데이터에서 rep 카운트를 왜곡할 수
있는 요인들이다.

### 8.1 Drive state gating은 필수

MCU의 `WP_DriveStatus`는 4단계 (`Debug → EncInit → Calibration → RunGym`).
부팅 직후, 엔코더 초기화 중, 위치 캘리브레이션 중에는 `position` /
`speed` / `fLoad` 값이 신뢰할 수 없다. EncInit 절차는 모터를 10개
각도 위치로 강제 회전시키며 Z-pulse를 찾는데, 이때 rep detection을
돌리면 가짜 rep이 줄줄이 잡힐 수 있다.

`SixStateFsmRepDetector`는 `SideSample.driveStatus.isWorkoutReady`를
체크해서 `RunGym` 외 상태에서는 sample을 무조건 drop하고 in-progress
rep을 reset한다. `motion_sample_mapper.dart`가 일단
`DriveStatus.runGym`을 default로 채우지만, 향후 MCU가 dev report에
drive state를 전송하기 시작하면 그걸 직접 사용해야 한다.

### 8.2 MotionAutoWeight와 rep 경계의 관계

MCU에는 자동 무게 on/off 기능이 있다 (`WESPION_App.c:959`):

| 동작 | MCU 트리거 |
|---|---|
| 자동 ON (FastPullOn) | speed > 520 mm/s for 100ms (양쪽) |
| 자동 OFF (DropRelease) | speed < -720 mm/s |
| 자동 OFF (AutoRest) | \|speed\| < 35 mm/s for 5000ms |

함의:
- 사용자가 5초 이상 멈추면 MCU가 weight off를 거는데, 이는 사용자가
  세트를 끝낸 게 아닐 수도 있다 (잠깐 fix하는 중일 수도). detector의
  `setRestTimeout`을 MCU와 같은 5000ms로 맞춰 둠으로써 양쪽 판단이
  일관된다 (`SixStateFsmConfig` defaults 참고).
- weight off 진입 시 `OnOffScale`이 ramp down (10ms당 0.01) → fLoad가
  급격히 감소. 이 구간에서 MCV 측정하면 부하가 가벼운 것처럼
  보인다. 향후 weight off 구간을 rep 판정에서 excluded로 마킹할 필요
  있음.

### 8.3 Thermal derating은 velocity loss를 오염시킨다

MCU는 `Iq²` 누적으로 모터 온도를 추정하고 (`tim.c:895-937`),
임계치 초과 시 `Iq_x10_Lim`을 30A → 5A까지 감소시킨다. 즉 **긴
세트의 후반부는 사용자가 지친 게 아니라 MCU가 전류를 줄여서
저항이 가벼워진다**.

이 상황에서 단순히 "rep N의 MCV vs rep 1의 MCV" 로 fatigue를
판정하면 모터 derating을 사용자 피로로 잘못 해석한다. 향후
`CompletedRep`에 derating 활성 여부 플래그를 추가하거나, fLoad
감소율을 보고 보정해야 정확하다. 현재 `velocityLossPct`는
이 효과를 분리하지 못한다.

### 8.4 Mode-aware metric 처리 (구현됨)

MCU 6모드 중 **Isokinetic / Hydraulic / Vibration**은 cable speed를
컨트롤러나 모드 정의에 의해 클램프/왜곡한다:

- **Isokinetic**: PI 컨트롤러가 speed를 `Iso.Vtarget`에 강제. 사용자가
  더 빨리 당기려 해도 클램프됨. measured speed는 user effort의
  함수가 아니다.
- **Hydraulic**: `F = base × (v/Vref)^n`. 속도가 빨라질수록 force가
  지수적으로 증가. MCV가 user effort와 비선형 관계.
- **Vibration**: 진동 성분 때문에 mean velocity가 noisy.

`StandardVbtMetricsCalculator`는 `WeightMode.velocityMeaningful` 체크로
이 모드들에서 velocity/power 메트릭을 자동으로 null 처리한다. ROM,
force, time 메트릭은 그대로 계산된다.

### 8.5 Region 기반 segmentation의 가능성

MCU가 이미 `WP_RegionStatus` enum (`block/ground/base/idle/loSoft/rom/
hiSoft/over`)으로 cable position을 분류해서 dev report bytes 34-35로
보낸다. 이는 user-calibrated ROM과 동기화되어 있어 노이즈가 적다.

대안 detector 아이디어: **`RegionBasedRepDetector`**
- `idle → rom` 전이 = rep 시작
- `rom → idle` 하강 전이 = rep 완료
- soft 구간(`loSoft`, `hiSoft`)은 turnaround dwell로 카운트
- `block` / `over`는 무시

이는 6-state FSM의 position/speed/hysteresis 튜닝 부담을 MCU에 위임
하는 효과. 6-state FSM과 동일한 합성 데이터에서 결과를 비교하는
benchmark를 통해 어느 쪽이 더 정확한지 결정할 수 있다. Phase 8
(실기기 데이터 수집) 이후 평가 후보.

### 8.6 Force interpretation: friction compensation

MCU의 `WP_Machine` 구조체에는 `FrictionWeight`, `FrictionCoeff`,
`FrictionSpeedPlus/Minus`가 있다. dev report의 `fLoad`는
`DevReport_FLoad[L/R]`라는 별도 변수에서 오는데, 이는 raw
`Icmd × scale`이 아니라 마찰 보정을 거친 값일 가능성이 높다
(`WESPION_App.c:681`).

VBT 메트릭의 "사용자가 실제로 느끼는 힘"은 마찰 포함이 맞다. 따라서
앱의 `meanConcentricForceKg = fLoad`는 VBT 정의와 일치한다.
icmd × scale을 직접 쓰면 마찰을 두 번 빼게 된다. **`fLoad`를
믿어라.**

### 8.7 BLE_CONNECT (0x06) 응답 패턴

MCU의 `0x06`은 단일 응답이 아니라 4개의 별도 프레임으로
파편화된다 (`protocol.c`):

1. `0x67 WEIGHT_MINUS` payload: weight + mode snapshot
2. `0x05 GET_VOLTAGE` payload: voltage
3. `0x65 WEIGHT_ON_OFF` payload: 전원 상태
4. `0x68 MODE_CHANGE` payload: 현재 모드

따라서 `BleConnectCommand`를 보낸 후 단일 응답을 기다리지 말고
이 4개 프레임이 도착하기를 기다려야 한다. `DeviceGatewayImpl`이 각각
`WeightResponse`, `VoltageResponse`, `WeightPowerResponse`,
`WeightModeResponse`로 파싱해서 별도 sub-state를 업데이트한다.

### 8.8 대기열에 있는 detector 보강 항목

위 발견들에서 파생된 후속 작업들:

- [ ] `RegionBasedRepDetector` 구현 + benchmark 비교 (8.5)
- [ ] `CompletedRep`에 `deratingActive` / `weightOffRamp` 플래그 (8.2, 8.3)
- [ ] `velocityLossPct` 계산에서 derating 구간 제외 (8.3)
- [ ] dev report에 drive state가 추가되면 mapper 갱신 (8.1)
- [ ] `MotionAutoWeight` 트리거 (weight off 직후) 를 set boundary 후보로 활용
