결론#
ManipForce를 NACHI MZ07에 올리는 데 LeRobot 채택은 순손실이다. 로봇 어댑터(REST 게이트웨이)만 교체하는 것이 압도적으로 싸다. 코드 근거는 franka 서버(ssh franka) 실측.
전제 정정 (자주 틀리는 것들)#
- "NACHI는 TCP만 가능"은 프로토콜 한계가 아니라 현재 구현 선택이다.
~/ros2_nachi/opennr/OpenNR-IF.h:312-327—fComAngle(관절각 명령)이 STD·EXT Set 구조체 양쪽에 모두 존재(EXT 7축, STD 8축). 전송계층도 TCP/UDP 상수 둘 다 제공(:21-26). 여기서 "TCP"는 네트워크가 아니라 Tool Center Point다. - 200 kHz가 아니라 200 Hz.
nachi_bringup/config/controllers.yaml:1-4update_rate: 200,src/test.py:20-23create_timer(0.005). - ManipForce의 "frequency-aware" FT 인코더는 활성 설정에서 주파수 인지가 아니다.
manipforce_ods3_256x256.yaml:74-80이use_ft_ncde: False→FTEmbed선택.model/vision/utils/ft_embed.py:28-81= kernel-3 Conv1d + GroupNorm + GELU + learnable alpha residual. FFT/STFT 없음. (논문 제목과 활성 코드가 다름) pose_wrt_start는 정책에 안 들어간다.config/task/manipforce.yaml:48-54ignore_by_policy: True,dataset/manipforce_dataset.py:349-359에서 제외.- 학습 데이터는 embodiment-free.
scripts/processing/get_wrist_pose_adv2.py:1-12— UMI 스타일 핸드헬드 리그 + ArUco 큐브(마커 51mm, 큐브 60mm). 수집 스크립트는 Franka를 아예 안 씀. → 로봇 바꿔도 데이터 재수집 불필요.
LeRobot이 안 되는 이유 (v0.6.2 실측)#
- 산업로봇 구현이 하나도 없다.
src/lerobot/robots/= SO/Koch/LeKiwi/Reachy2/Unitree G1/OpenArm/reBot/OMX. UR도 Franka도 없음. - Robot ABC 자체는
RobotAction = dict[str, Any](lerobot_types.py:39-42)라 카르테시안 수용은 구조적으로 가능(robots/robot.py:102-113). - 그러나 F/T 모달리티가 표현이 안 된다 ← 결정타.
configs/types.py:20-27FeatureType = STATE/VISUAL/ENV/ACTION/REWARD/LANGUAGE. force/torque 전용 없음.utils/feature_utils.py:68-103hw_to_dataset_features()가 모든 float를observation.state한 벡터로 합치고, tuple은 전부 카메라로 분류해(H,W,1|3)아니면 오류 →ft_data: (6,)를 Robot feature로 선언 불가.- 비대칭 horizon 불가. dataset reader는 키별 다른
delta_timestamps길이를 허용(datasets/dataset_reader.py:245-293)하지만,datasets/factory.py:55-63이 모든observation.*에 동일observation_delta_indices를 배포하고policies/diffusion/configuration_diffusion.py:250-256이 단일n_obs_steps를 씀 → 이미지 horizon 2 / FT horizon 8 구성 불가. - 정책 구조가 열등하다. LeRobot diffusion = ImageNet ResNet18 + SpatialSoftmax(32 keypoint→64D) + concat/flatten(
modeling_diffusion.py:270-306, 500-559). ManipForce의 DINOv2 patch token + 이미지↔FT 양방향 cross-attention(fmt_obs_encoder.py:515-564) 전부 상실. - 체크포인트 비호환. ManipForce
TransformerForActionDiffusion+ dill.ckpt(cfg+state_dicts+workspace,workspace/base_workspace.py:62-102) vs LeRobotDiffusionConditionalUnet1d+model.safetensors(policies/pretrained.py:101-188). → native 교체 시 재학습 필수. - 200 Hz 보장 없음. stock record/rollout 루프는 순차 실행 후 잔여 sleep하는 soft real-time이며 5ms 초과 시 경고만(
scripts/lerobot_record.py:277-356,rollout/strategies/base.py:49-76).
비용 산정#
| 방안 | 신규 파일 | LOC | ManipForce 수정 | 재학습 |
|---|---|---|---|---|
| (A) NACHI REST 게이트웨이 | 3 | 380~540 | 0 | 불필요 |
(E) rosaic-physical-ai nachi_handeye_ft_real + 실기 런치 |
4~7 | 500~900 | 0 | 불필요 |
| (C) LeRobot 데이터 레이어만 | — | — | — | FT 표현 불가로 이득 없음 |
| (B) LeRobot 런타임 채택 | 5~6 | 550~850 | 대규모 | 필수 + 성능 손실 |
(A) 근거: 로봇 결합부가 HTTP REST 7종뿐이고 호출 지점 24곳(eval_robot_v2.py 13 + eval_helpers.py 11). action_builder.py는 로봇 클라이언트를 import조차 안 함(순수 수학). 포트 5000에서 동일 계약 제공 시 ManipForce 0수정.
발견한 결함 3건#
- OpenNR 프로토콜 계약 위반.
nachi_tcp_tracking_hwif.cpp:229-234가ushProtcolBit=1(EXT)로 두고 데이터는ustData.stStd.fComTcpPos(STD)에 씀(:247-266). 헤더 규칙은 bit=1→stExt(OpenNR-IF.h:329-335).fComTcpPos가 두 구조체 첫 필드라 offset이 우연히 겹쳐 동작하지만, 뒤의fWorkCoord/fToolCoord는 어긋남. MZ07 전 정리 필요(~3 LOC). - REST 클라이언트/서버 드리프트.
franka_api.py가 호출하는/health,/pose_sync,/set_gripper라우트가custom_server.py(활성 route 20개)에 없음. - 포트 인자 무시.
flask_pose_client.py:90-94connect_to_server(host, port=4999)가 인자를 버리고rest_port=5000하드코딩.
진짜 리스크 = 코드가 아니라 접촉 물리#
- 명령형 힘제어 의존: 없음 (나가는 건 EE pose + gripper뿐,
action_builder.py:45-55, 88-105). - 힘 관측 의존: 있으나 이식 가능 — Aidin 외장 UDP F/T + IMU 중력보상(
hardware_init.py:40-62,gravity_compensation_utils.py:137-189). FR3 내장 기능 아님. - FR3 Cartesian impedance 실행기 의존: 있음 ← 핵심.
custom_server.py:135-147, 215-223이 포즈를/cartesian_impedance_controller/equilibrium_pose로 publish,:432-475precision 모드가 stiffness/damping을 동적 변경. FR3에서 "임피던스 평형점"이던 숫자가 NACHI에선 "순수 위치 명령"이 되어 접촉 시 의미가 다름. - FR3 7축 vs CMZ05/MZ07 6축 → redundancy 없어 동일 EE pose 도달성 비동등.
- 대응: bounded position correction + 속도/가속도/저크 제한 + 수동 컴플라이언스(RCC) + MZ07 데이터 파인튜닝. 성공률 이식 가능성은 실기 실험으로만 답이 나옴.
MZ07 실물 상태 (2026-08-10)#
- 실물 없음.
ros2_nachi는 CMZ05 대상(bringup.launch.py:cmz05,192.168.1.150:10081). rosaic-physical-ai/runtime/src/robots/nachi/README.md: "vendor가 mz07 URDF를 안 줘서 cmz05 암으로 대체(동일 계열)".- franka 서버에 192.168.1.x 인터페이스 없음, OpenNR 포트 10081 무응답.
ws_tcp_tracking에build/·install/없음 → 이 서버에서 빌드된 적 없는 벤더 스냅샷(일본어 주석, git 메타데이터 없음).
이미 있는 자산 (재발명 금지)#
~/rosaic-physical-ai = ManipForce 그린필드 후속. hexagonal + canon 레이어로 embodiment/정책/전송 분리(N×M → N+M).
- Description 8종: fr3_sim, fr3_handeye_ft_sim, fr3_handeye_ft_real, rm75_sim, rm75_handeye_ft_sim, nachi_sim, nachi_handeye_ft_sim, gumi_handheld
- Specification 2종: manipforce, octo. 695 tests 통과.
- 빠진 것: nachi_handeye_ft_real.py(~100 LOC), NACHI 실기 런치 전무(sim 2개뿐), registry/runtime map에 real 미등록(control/runs/registry.py:97-122, control/adapters/outbound/runtime_control_local.py:24-40), cmz05_manipforce_sim.launch.py는 runtime_node 미와이어로 open-loop.
권고#
(A)로 실기 검증 먼저 → (E)로 수렴. (A)는 즉시 실행 가능하고 변경 경계가 작으며 추론 코드 0수정. (E)는 다로봇 플랫폼으로서 구조적으로 옳지만 실기 런치가 없어 지금 당장의 경로는 아니다. LeRobot은 (B)(C) 모두 채택 이유 없음.
관련: lg-poc-mz07-heterogeneous-cell codex-implements-i-review franka-server-access
추가 검증 (2차 코덱스 교차검증, 직접 확인 완료)#
두 번째 코덱스 에이전트가 같은 결론(A ≪ B)에 도달했으나 인용 일부가 허구였다. 직접 ssh franka로 재확인한 결과:
허구로 확인된 인용 (전파 금지)#
ws_tcp_tracking/src/nachi_cmz05_hardware/— 존재하지 않음. 실제 경로는src/nachi_tcp_tracking_hwif/.ws_tcp_tracking/src/nachi_joint_reader/— 존재하지 않음. 실제는src/joint_reader/src/joint_reader_node.cpp.nachi_bringup/urdf/ros2_control.xacro— 존재하지 않음. 실제는nachi_description/urdf/cmz05/ros2_control.xacro.flask_pose_client.send_pose_to_franka()— 존재하지 않음. 실제 함수명은send_pose_command().
교훈: 코덱스 병렬 실행 시 파일 경로/심볼명 인용은 반드시 재확인할 것. 결론은 맞아도 근거가 조작될 수 있다.
실재 확인된 신규 발견 (직접 grep으로 검증)#
~/ros2_nachi/opennr/OpenNR-IF.h에 비-스트리밍 모션 API가 따로 있다:
- NR_CtrlMoveJ(nOpenId, fAngle[], nAngleSize, nType, nUnitId) — 절대 관절각 이동, :589
- NR_CtrlMoveJA(...) — 상대 관절각 이동, :599
- NR_CtrlMoveX :555 / NR_CtrlMoveXR :567 / NR_CtrlMoveXT :579 — 절대 / robot-relative / tool-relative TCP 이동 (NR_POSE*)
- nType = 0:Path / 1:Pause / 2:End
즉 OpenNR은 (1) NR_SetAll 실시간 스트리밍 + (2) NR_CtrlMove* 개별 모션 명령, 두 경로를 제공한다. 우리 드라이버는 (1)만 쓴다.
NR_PUSH_MODE_* 상수 사다리 존재 (:127-134, "Priority" 섹션):
NR_PULL_MODE(-1) / PUSH_MODE_5(0) / _10(1) / _50(2) / _100(3) / _200(4) / _500(5) / _1000(6).
단위(ms 주기 vs Hz)는 헤더만으로 미확인. ms 해석이면 PUSH_MODE_5=5ms=200Hz가 최속이고 현재 update_rate: 200과 정합. 우리 코드에서 이 상수는 어디서도 쓰이지 않는다(grep 결과 define 7줄뿐). NACHI에 서면 확인 필요.
실시간성 리스크 (헤더+코드 직접 확인)#
nachi_tcp_tracking_hwif.cpp의 write가 NR_SetAll(open_id_real_, &s, NR_ACCESS_WAIT) — blocking 호출이고, on_configure에서 info.lSendTimeOut = 100(ms)로 연결한다. 즉 update_rate: 200(5ms deadline)은 목표 스케줄링 rate이지 보장된 deadline이 아니다. 최악 100ms까지 블록될 수 있다.
실기 투입 전 계측 필수: controller_manager cycle time, NR_SetAll latency p50/p95/p99/max, watchdog timeout, target stale 시 hold/stop 거동.
정정 — 앞의 "LeRobot 채택 이유 없음"은 과했다 (3차 검증)#
사용자 반문("산업로봇 예시가 다른 레포에도 아예 없나? FT가 아예 안 되나?")을 받아 재검증한 결과, 위 결론 중 두 가지가 틀렸다. 제어 경로 결론(A 우선)은 유지되지만 데이터 레이어 결론은 뒤집힌다.
틀린 것 ①: "산업로봇 지원 0개 → 확장 불가"#
in-tree 0개는 사실이나, LeRobot에는 플러그인 시스템이 있고 산업로봇 생태계가 실재한다.
- src/lerobot/utils/import_utils.py:223-237 register_third_party_plugins() — 설치된 배포판 이름이 lerobot_robot_ / lerobot_camera_ / lerobot_teleoperator_ / lerobot_policy_ / lerobot_env_ 로 시작하면 자동 import. entry_points 아님, 패키지명 스캔 + RobotConfig.register_subclass() + class-name convention(XxxConfig→Xxx) 방식. 동적 탐색은 import_utils.py:158-220.
- CLI가 실행 전 호출: scripts/lerobot_record.py:546-548, scripts/lerobot_train.py:949-956.
- docs/source/third_party_robots.mdx 에 "Industrial & Collaborative Arms" 섹션이 존재(15,635 bytes, 실재 확인): xArm(UFACTORY), Lebai 6축 협동로봇, UR5e 3종(그중 lerobot_robot_ur5e는 RTDE+Robotiq drop-in 플러그인), Franka용 LeFranX(fork), Trossen, AgileX Piper 2종.
- 즉 lerobot을 fork하지 않고 lerobot_robot_nachi 별도 패키지로 붙일 수 있다. 최소 구조 = pyproject.toml + __init__.py + config_nachi.py + nachi.py.
- 추가: EE(카르테시안) 액션도 1급 지원. docs/source/action_representations.mdx — Joint space vs End-Effector space, RobotKinematics + FK/IK processor, 그리고 absolute/relative/delta 용어를 UMI(Chi et al. 2024) 기준으로 따른다고 명시(ManipForce와 같은 계보).
틀린 것 ②: "F/T 표현 불가"#
구조적으로 불가능하지 않다. 3층으로 갈라야 한다.
- 저장층: 가능. LeRobotDataset.create()는 호출자의 features dict를 그대로 받는다(datasets/lerobot_dataset.py:683-705). (6,)→datasets.Sequence, (8,6)→datasets.Array2D (datasets/feature_utils.py:55-80).
- 읽기층: 가능. 생성자의 delta_timestamps를 그대로 reader에 전달하며(lerobot_dataset.py:47-55, 257-268), 검사는 각 delta가 1/fps 정수배인지만 볼 뿐 키 간 길이 일치 assert가 없다(datasets/feature_utils.py:160-201). 키별 독립 인덱싱·스택(dataset_reader.py:240-293).
- 막히는 곳(3개, 전부 우회 가능):
1. 표준 recorder의 hw_to_dataset_features()가 비영상을 observation.state 하나로 합침(utils/feature_utils.py:68-103) → 직접 create()/add_frame() 호출로 우회.
2. datasets/factory.py:55-63 resolve_delta_timestamps()가 모든 observation.*에 단일 observation_delta_indices 적용 → 직접 LeRobotDataset(..., delta_timestamps=...)면 미실행. 단 표준 lerobot-train은 factory 경유(scripts/lerobot_train.py:426-432).
3. 기존 SmolVLA/PI0는 batch[OBS_STATE]만 소비(smolvla/modeling_smolvla.py:409-417, pi0/modeling_pi0.py:1048-1055) → feature만 추가한다고 FT fusion 안 됨. 커스텀 policy 필요.
- 커스텀 PreTrainedPolicy는 batch["observation.ft_data"]를 직접 받는다(policies/pretrained.py:217-247; 학습 루프가 dict 전체를 policy(batch)로 전달, scripts/lerobot_train.py:710-729). processor도 확장점 보유(processor/pipeline.py:150-189, 223-238, 1131-1193), converters.py:331-404가 observation.* 전 키를 보존.
- 작업량: policy config 1 + modeling wrapper 1 + dataset 변환 adapter 1 + optional processor 1 = 3~4파일, 300~600 LOC(추정, 미검증).
핵심 설계 통찰 (병목 소멸 트릭)#
FT를 프레임마다 (8,6) 윈도로 미리 말아 저장하면 horizon 충돌이 사라진다 — 윈도가 시간축이 아니라 feature 내부에 들어가므로 모든 observation.*가 동일 horizon(2)을 공유할 수 있고, 그러면 위 병목 ②(factory)가 자동 해소되어 표준 lerobot-train CLI를 그대로 쓸 수 있다. 저장 비용은 무시 가능: 8×6 float32 = 192 B/frame → 30 fps × 1시간 ≈ 20 MB.
남는 진짜 손실 (이건 여전히 사실)#
LeRobot은 단일 row clock 전제다. 현재 zarr는 이미지와 F/T의 샘플 수·타임스탬프·에피소드 경계를 완전히 독립으로 유지하고(change_to_zarr.py:638-666, 723-756), 두 이미지 타임스탬프 사이의 200 Hz F/T를 searchsorted로 뽑아 정확히 8개로 pad/downsample한다(common/sampler.py:628-703). 이 로직은 사라지는 게 아니라 export adapter로 이동한다.
수정된 권고#
- 제어/추론 경로: 변경 없음. LeRobot 루프는 soft real-time(5 ms 초과 시 경고만,
scripts/lerobot_record.py:277-356)이라 200 Hz 스트리밍에 부적합. NACHI 게이트웨이/ros2_control이 200 Hz를 소유해야 한다. 게다가 우리 데이터 수집은 로봇을 안 쓴다(핸드헬드 리그) →lerobot_robot_nachi가 수집에 기여하는 바가 없다. - 데이터 레이어: C안이 올라간다. 얻는 것 = 포맷 버저닝(
CODEBASE_VERSION="v3.0"), 자동 청킹, MP4 인코딩(병렬/스트리밍), min/max/mean/std/quantile 통계, HF Hub 업로드·부분 다운로드·dataset card. 우리 수집이 로봇-독립이라 순수 export/convert 작업이다. - 하이브리드가 정답: 수집·F/T 검증은 현행 유지 → 검증 통과 에피소드를 LeRobot 포맷(2-cam + pre-windowed 8×6 FT + 10D action)으로 export.
- 속도·디스크가 현 zarr(uint8 + Blosc/Zstd) 대비 실제 개선되는지는 미확인 — LeRobot은 MP4 디코드 경로라 워크로드별 실측 필요.
Sagwan Revalidation 2026-08-31T11:59:20Z#
- verdict:
refresh - note: LeRobot 로봇·모달리티 지원은 빠르게 변해 v0.6.2 근거 재확인이 필요하다
Sagwan Revalidation 2026-09-03T03:01:54Z#
- verdict:
ok - note: 직전 검증 후 변화 가능성이 낮고 핵심 판정 근거도 일관됨
Sagwan Revalidation 2026-09-09T05:51:55Z#
- verdict:
ok - note: [chatgpt HTTP 404] {
Sagwan Revalidation 2026-09-11T19:49:19Z#
- verdict:
ok - note: LeRobot 공식 지원 목록에 산업용 로봇(UR·Franka·NACHI) 미포함 상태 유지 — 어댑터 교체 우위 결론은 여전히 유효.
Sagwan Revalidation 2026-09-15T14:07:53Z#
- verdict:
ok - note: 관련 반박·상위 갱신 노트 없음, 기존 판정은 여전히 재사용 가능