# 물리 모델 — 기기가 무엇을 측정하는가

> **핵심**: Roomfit은 *전류 제어 기반* 디지털 웨이트 머신이다. 물리적 추(weight plate)가 없다.
> **모터 전류(`icmd`)가 곧 토크가 되고, 그 토크가 케이블을 통해 사용자에게 저항으로 전달된다.**
> 즉 `전류 = 토크 = 사용자가 느끼는 무게`.

## 인과 흐름

```
사용자가 케이블을 당김
  → 엔코더가 position 변화 감지
  → MCU가 (모드 + region + 설정무게)에 따라 전류 명령(icmd) 계산
  → 모터가 icmd만큼 전류를 흘려 토크(저항) 생성
  → ifb로 실제 전류 측정 (icmd와 차이 = 제어 오차)
  → fLoad = icmd를 kg으로 역산한 값 (사용자 체감 무게)
```

→ **icmd는 디버깅용이 아니라 이 기구의 핵심**. fLoad는 icmd의 파생값.

## 핵심 신호 5종

| 필드 | 단위 | 의미 | 비고 |
|---|---|---|---|
| `position` | 0.05mm | 케이블 위치 | 엔코더 raw, monotonic 보정됨 |
| `speed` | mm/s, signed | 케이블 속도 | + = extending(당기는 중) |
| `accel` | mm/s², signed | 케이블 가속도 | speed의 미분, 50ms 주기 |
| `icmd` | mA | 모터 명령 전류 | **저항의 ground truth** |
| `ifb` | mA | 모터 피드백 전류 | 실제 흐른 전류 |
| `fLoad` | 0.01kg | 계산된 부하 | icmd × kt / r 환산값 |

좌우 모터가 독립이라 **모든 신호가 `L/R` 쌍**으로 들어옴.

## 운동 역학 (Kinematics)

`position → speed(미분) → accel(2차 미분)`. MCU가 50ms 주기로 계산.

- **speed > 0** (concentric phase): 당기는 중, 근육 수축. VBT 메트릭 측정 구간.
- **speed < 0** (eccentric phase): 놓는 중, 근육 이완.
- **accel**: 폭발력 지표. `power = fLoad × speed`.

## 무게 모드 — `WeightMode` (6종)

MCU 펌웨어의 `WP_WeightMode` enum과 1:1 대응. 소스: `docs/reference/mcu-source/Core/Inc/WESPION_Def.h`.

| 코드 | 모드 | 동작 | 핵심 파라미터 | VBT 의미 |
|---|---|---|---|---|
| 0 | **Normal** | 일정 저항 (position/speed 무관) | `WeightSet` | ✅ MCV/PCV 의미 |
| 1 | **Eccentric** | concentric은 정상, eccentric에서 force 가산 | `F_EccSet`, `EccSpdUp/Down` | ✅ |
| 2 | **Elastic** | position이 커질수록 force 점증 | 내부 lookup | ✅ |
| 3 | **Isokinetic** | PI 컨트롤러가 cable speed를 `Vtarget`에 클램프 | `Iso.Vtarget`, `Iso.Kp` | ❌ velocity가 컨트롤러에 묶임 |
| 4 | **Hydraulic** | `F = baseF × (speed/Vref)^n` 속도-파워 법칙 | `Hydro.Vref`, `Hydro.n` | ❌ velocity가 force 정의에 들어감 |
| 5 | **Vibration** | 기본 force에 sin 진동 추가 | `Vib.Freq`, `Vib.AmplitudeKg` | ❌ |

`WeightMode.velocityMeaningful` 플래그가 위 표의 ✅/❌를 코드에서 표현.
`StandardVbtMetricsCalculator`가 자동으로 비-VBT 모드의 velocity/power 메트릭을 `null` 처리.

## Region — `RegionStatus` (8단계)

MCU가 cable position을 zone으로 분류 (`WESPION_Def.h:122-132`):

| 코드 | 이름 | 의미 |
|---|---|---|
| -1 | `block` | 안전 차단. rep 카운트 금지 |
| 0 | `ground` | 케이블 완전 수축. 기계적 바닥 |
| 1 | `base` | 베이스 휴식. 사용자 대기 |
| 2 | `idle` | base ↔ ROM 사이 자유 영역. rep 시작 전 |
| 3 | `loSoft` | ROM 저단 soft entry. force ramp-in |
| 4 | `rom` | calibrate된 Range-of-Motion. **실제 운동 구간** |
| 5 | `hiSoft` | ROM 상단 soft exit. force ramp-out |
| 6 | `over` | ROM 초과. 안전 이벤트 |

Rep detection 핵심:
- `idle → rom` 전이 = rep 시작
- `rom → idle` 하강 전이 = rep 완료

## Drive Status — `DriveStatus` (4단계)

부팅 → 캘리브레이션 → 운동 모드 (`WESPION_Def.h:46-52`):

| 코드 | 이름 | 의미 |
|---|---|---|
| 0 | `debug` | 개발 상태, 모터 게이트 off |
| 1 | `encoderInit` | 엔코더 초기화 진행 중. **position 신뢰 X** |
| 2 | `calibration` | 위치 캘리브레이션 진행 중. position settling 중 |
| 3 | `runGym` | **정상 운동 모드.** position/speed/force가 valid한 유일한 상태 |

→ Rep detection은 `runGym`일 때만 sample을 feed.
`SixStateFsmRepDetector`는 `SideSample.driveStatus.isWorkoutReady` 체크해서 다른 상태 sample 무조건 무시.

## 전압과 안전

`voltage` = DC 전원부 전압 (V).

- 모터가 큰 전류를 소모하면 순간 전압 강하 발생.
- 정상 범위 이탈 시 `FailSafeError` 발생 → 기구 비상 정지.

## 좌/우 독립

모든 물리량이 L/R 독립. 좌우 모터는 별도로 제어되며,
무게/모드/속도가 각각 다를 수 있다 (예: unilateral 운동).

## "왜 이게 중요한가" — 차별화 포인트

상용 VBT 시스템(GymAware, Vitruve 등)은:
- 바벨에 IMU 부착 → accel + 추정된 force
- 사용자가 입력한 무게값에 의존
- **force는 추정값**

Roomfit은:
- `icmd → 토크 → force`가 물리적으로 확정
- **force가 ground truth**
- → RFD, Impulse, Power 모두 추정이 아닌 직접 측정
- → 업계 누구도 못 하는 차별화

## 관련 코드

- `packages/roomfit_protocol/lib/src/constants/command_codes.dart` — `WeightMode`, `RegionStatus`, `DriveStatus` enum
- `packages/roomfit_protocol/lib/src/responses/report_response.dart` — 36바이트 dev report 디코더
- `packages/roomfit_exercise/lib/src/sample/side_sample.dart` — `SideSample` (rep detection의 입력 단위)
- `docs/reference/mcu-source/Core/Inc/WESPION_Def.h` — MCU 측 enum 원본
- `CLAUDE.md` 루트 — "물리 모델 (MANDATORY)" 섹션
