안녕하세요?
먼저 저번에 주신 답변이 많이 도움이 되었습니다. 감사합니다. 다름이아니라 Lerobot에 FR 로봇과 UR 로봇을 활용하는 프로젝트를 진행하고 있는데 여쭤보고 싶은 점이 있어서 문의글을 남기게되었습니다.
- FR과 UR 로봇을 Lerobot에 연동시키는 작업을 진행하려하고 있는데, 이를 하기위해 참고할 수 있는 코드나 PiPER처럼 지원되는 코드가 있는지를 문의드리고 싶습니다.
항상 감사합니다.
안녕하세요?
먼저 저번에 주신 답변이 많이 도움이 되었습니다. 감사합니다. 다름이아니라 Lerobot에 FR 로봇과 UR 로봇을 활용하는 프로젝트를 진행하고 있는데 여쭤보고 싶은 점이 있어서 문의글을 남기게되었습니다.
항상 감사합니다.
안녕하세요! 저번 답변이 도움이 되셨다니 다행입니다.
FR (Fairino) 로봇과 UR (Universal Robots) 모두 LeRobot에 공식 네이티브 지원은 없습니다. 다만, LeRobot은 Robot 인터페이스를 직접 구현하면 어떤 커스텀 로봇이든 연동할 수 있도록 확장성 있게 설계되어 있습니다. PiPER처럼 공식 지원되는 수준의 코드는 현재 없고, 직접 구현이 필요합니다.
참고할 수 있는 것:
FR 시리즈는 Python, C#, C++, ROS, ROS2를 통한 프로그래밍을 지원합니다. 즉, Python SDK를 통해 LeRobot의 Robot 클래스를 래핑할 수 있습니다.
접근 방법:
Robot 베이스 클래스 구현참고 레퍼런스:
https://github.com/frRobot (Fairino GitHub)fr_robot.py, configuration_fr.py 등 작성UR은 LeRobot 공식 지원은 없지만, Python 레벨 제어 라이브러리가 풍부합니다.
추천 접근 방식 — python-urx 또는 RTDE 활용:
# python-urx 예시
import urx
rob = urx.Robot("192.168.0.100") # 로봇 IP
rob.movej((j1, j2, j3, j4, j5, j6), acc, vel)
joints = rob.getj() # 현재 관절값 읽기
이 SDK를 LeRobot Robot 인터페이스로 래핑하면 됩니다.
공식 UR 드라이버:
LeRobot의 Robot 추상 클래스를 상속받아 별도의 설치 가능한 Python 패키지로 만들면, lerobot-teleop, lerobot-record 등 CLI 도구들이 자동으로 인식합니다.
최소 구현해야 할 메서드:
from lerobot.robots.robot import Robot
class FRRobot(Robot):
def connect(self): ... # SDK 초기화 및 연결
def disconnect(self): ... # 연결 해제
def get_observation(self): ... # 현재 관절값 + 카메라
def send_action(self, action): ... # 관절 명령 전송
| 항목 | 링크/방법 |
|---|---|
| LeRobot 공식 커스텀 하드웨어 가이드 | github.com/huggingface/lerobot/blob/main/docs/source/integrate_hardware.mdx |
| PiPER 연동 PR (구조 참고) | github.com/huggingface/lerobot/pull/645 |
| UR Python 제어 라이브러리 | github.com/SintefManufacturing/python-urx |
| UR ROS2 공식 드라이버 | github.com/UniversalRobots/Universal_Robots_ROS2_Driver |
| RoboCOIN (Realman 등 SDK 기반 연동 예시) | pypi.org/project/robocoin/ |
특히 RoboCOIN 패키지는 LeRobot v2.1 기반으로 SDK 제어(Piper, Realman 등)와 ROS/MoveIt 기반 범용 제어를 모두 지원하는 통합 인터페이스를 제공합니다. FR/UR 연동 구조를 설계할 때 이 코드 구조를 참고하시면 많이 도움이 될 것 같습니다.
“”"
UR (Universal Robots) - LeRobot Robot 인터페이스 구현 예시
SDK 직접 제어 (Python) 방식 — RTDE 인터페이스 활용
필요 패키지:
pip install ur-rtde # UR 공식 Python RTDE 라이브러리
pip install numpy
참고:
UR RTDE 문서: Universal Robots RTDE C++ Interface — ur_rtde 1.6.3 documentation
지원 모델: UR3, UR5, UR10, UR16, UR20, UR30 (e-Series, CB3)
“”"
from future import annotations
import time
from dataclasses import dataclass, field
from typing import Any
import numpy as np
try:
import rtde_control
import rtde_receive
except ImportError:
raise ImportError(
“ur-rtde가 설치되어 있지 않습니다.\n”
“pip install ur-rtde 를 실행해주세요.\n”
“자세한 내용: Universal Robots RTDE C++ Interface — ur_rtde 1.6.3 documentation”
)
from lerobot.robots.robot import Robot, RobotConfig
@RobotConfig.register_subclass(“universal_robots_ur”)
@dataclass
class URRobotConfig(RobotConfig):
“”“Universal Robots 설정”“”
# 로봇 IP 주소
robot_ip: str = "192.168.1.100"
# 관절 수 (UR 시리즈 모두 6축)
num_joints: int = 6
# 관절 이름 (LeRobot 데이터셋에서 사용)
joint_names: list[str] = field(
default_factory=lambda: [
"shoulder_pan", "shoulder_lift", "elbow",
"wrist_1", "wrist_2", "wrist_3"
]
)
# 관절 제한 (단위: radian)
joint_limits_lower: list[float] = field(
default_factory=lambda: [-6.28] * 6 # -2π
)
joint_limits_upper: list[float] = field(
default_factory=lambda: [6.28] * 6 # +2π
)
# 제어 주파수 (Hz) — UR e-Series는 최대 500Hz RTDE 지원
control_hz: float = 50.0
# MoveJ 속도 (rad/s)
move_speed: float = 0.5
# MoveJ 가속도 (rad/s²)
move_acceleration: float = 0.5
# servoJ 제어 파라미터 (실시간 관절 제어 시)
servo_lookahead_time: float = 0.1 # 0.03 ~ 0.2초
servo_gain: float = 300.0 # 100 ~ 2000
# RTDE 주파수 (Hz)
rtde_frequency: float = 50.0
# 카메라 설정 (선택)
cameras: dict = field(default_factory=dict)
class URRobotArm(Robot):
“”"
Universal Robots (UR) LeRobot 연동 클래스
RTDE 인터페이스 기반 Python 직접 제어
사용 예시:
config = URRobotConfig(robot_ip="192.168.1.100")
robot = URRobotArm(config)
robot.connect()
obs = robot.get_observation()
print("현재 관절값(rad):", obs["observation.state"])
# 현재 위치에서 미세 이동
action = obs["observation.state"].copy()
action[0] += 0.1 # 첫 번째 관절 0.1 rad 이동
robot.send_action(action)
robot.disconnect()
주의:
실제 로봇 사용 전에 반드시 URSim(시뮬레이터)에서 테스트하세요.
PolyScope에서 External Control URCap이 실행 중이어야 합니다.
"""
def __init__(self, config: URRobotConfig):
super().__init__(config)
self.config = config
self._rtde_c: rtde_control.RTDEControlInterface | None = None
self._rtde_r: rtde_receive.RTDEReceiveInterface | None = None
self._is_connected = False
# ── 연결 / 해제 ──────────────────────────────
def connect(self) -> None:
"""UR 로봇에 RTDE로 연결합니다."""
print(f"[UR Robot] {self.config.robot_ip} 에 연결 중...")
# RTDE 수신 인터페이스 (관절 상태 읽기)
self._rtde_r = rtde_receive.RTDEReceiveInterface(
self.config.robot_ip,
self.config.rtde_frequency,
)
# RTDE 제어 인터페이스 (명령 전송)
self._rtde_c = rtde_control.RTDEControlInterface(
self.config.robot_ip,
)
if not self._rtde_r.isConnected():
raise ConnectionError(
f"UR RTDE Receive 연결 실패. IP({self.config.robot_ip})를 확인하세요."
)
if not self._rtde_c.isConnected():
raise ConnectionError(
f"UR RTDE Control 연결 실패. IP({self.config.robot_ip})를 확인하세요.\n"
"힌트: PolyScope에서 External Control 프로그램이 실행 중인지 확인하세요."
)
self._is_connected = True
joints = self._rtde_r.getActualQ()
print(f"[UR Robot] 연결 완료! 현재 관절값(rad): {np.round(joints, 3)}")
# 카메라 연결
for cam_name, cam_config in self.config.cameras.items():
cam_config.connect()
print(f"[UR Robot] 카메라 '{cam_name}' 연결 완료")
def disconnect(self) -> None:
"""RTDE 연결을 해제합니다."""
if self._rtde_c is not None:
self._rtde_c.disconnect()
self._rtde_c = None
if self._rtde_r is not None:
self._rtde_r.disconnect()
self._rtde_r = None
self._is_connected = False
print("[UR Robot] 연결 해제 완료")
# ── 관측값 (Observation) ─────────────────────
def get_observation(self) -> dict[str, Any]:
"""
현재 로봇 상태를 반환합니다.
Returns:
dict with keys:
"observation.state": 관절 위치 (radian, shape: [6])
"observation.images.<cam_name>": 카메라 이미지 (선택)
"""
self._check_connected()
obs = {}
# 현재 관절 위치 읽기 (radian 단위)
joints = np.array(self._rtde_r.getActualQ(), dtype=np.float32)
obs["observation.state"] = joints
# (선택) TCP 포즈도 필요하면 추가
# tcp_pose = np.array(self._rtde_r.getActualTCPPose(), dtype=np.float32)
# obs["observation.tcp_pose"] = tcp_pose
# 카메라 이미지
for cam_name, cam in self.config.cameras.items():
img = cam.read()
obs[f"observation.images.{cam_name}"] = img
return obs
# ── 액션 전송 (Action) ───────────────────────
def send_action(self, action: np.ndarray) -> np.ndarray:
"""
관절 위치 명령을 전송합니다.
두 가지 모드:
- MoveJ: 블로킹 방식, 안정적 (기본값)
- servoJ: 논블로킹, 실시간 제어 (고주파 제어 시 사용)
Args:
action: 목표 관절 위치 (radian, shape: [6])
Returns:
실제 전송된 action (클리핑 후)
"""
self._check_connected()
# 관절 한계 클리핑
lower = np.array(self.config.joint_limits_lower)
upper = np.array(self.config.joint_limits_upper)
action = np.clip(action, lower, upper)
# ── MoveJ 방식 (안정적, 블로킹) ──────────
self._rtde_c.moveJ(
action.tolist(), # 목표 관절 위치 (radian)
self.config.move_speed, # 속도 (rad/s)
self.config.move_acceleration, # 가속도 (rad/s²)
asynchronous=True, # True: 논블로킹 (다음 명령 바로 전송 가능)
)
# ── servoJ 방식 (실시간 제어, 고주파수 루프에서 사용) ──
# dt = 1.0 / self.config.control_hz
# self._rtde_c.servoJ(
# action.tolist(),
# self.config.move_speed,
# self.config.move_acceleration,
# dt, # timestep
# self.config.servo_lookahead_time,
# self.config.servo_gain,
# )
return action
# ── 유틸 ─────────────────────────────────────
def _check_connected(self):
if not self._is_connected:
raise RuntimeError("로봇이 연결되어 있지 않습니다. connect()를 먼저 호출하세요.")
@property
def is_connected(self) -> bool:
return self._is_connected
def get_state(self) -> np.ndarray:
"""현재 관절 위치만 반환하는 편의 함수"""
return self.get_observation()["observation.state"]
def get_tcp_pose(self) -> np.ndarray:
"""현재 TCP 포즈 반환 [x, y, z, rx, ry, rz]"""
self._check_connected()
return np.array(self._rtde_r.getActualTCPPose(), dtype=np.float32)
def stop(self) -> None:
"""로봇 즉시 정지"""
if self._rtde_c is not None:
self._rtde_c.stopJ(2.0) # 감속도 2.0 rad/s²
print("[UR Robot] 정지 완료")
def move_home(self) -> None:
"""홈 포지션으로 이동 (모든 관절 0)"""
home = np.zeros(self.config.num_joints)
self.send_action(home)
time.sleep(3.0)
print("[UR Robot] 홈 포지션 이동 완료")
“”"
FR (Fairino) Robot - LeRobot Robot 인터페이스 구현 예시
SDK 직접 제어 (Python) 방식
필요 패키지:
pip install fairino # Fairino 공식 Python SDK
pip install numpy
참고: Fairino SDK 문서 - GitHub - FAIR-INNOVATION/fairino-python-sdk: Python language SDK library specially designed for fairino collaborative robots. · GitHub
“”"
from future import annotations
import time
from dataclasses import dataclass, field
from typing import Any
import numpy as np
try:
from fairino import Robot as FaiRobot
except ImportError:
raise ImportError(
“Fairino SDK가 설치되어 있지 않습니다.\n”
“pip install fairino 또는 Fairino 공식 SDK를 설치해주세요.”
)
from lerobot.robots.robot import Robot, RobotConfig
@RobotConfig.register_subclass(“fairino_fr”)
@dataclass
class FaiRobotConfig(RobotConfig):
“”“Fairino FR 로봇 설정”“”
# 로봇 IP 주소
robot_ip: str = "192.168.58.2"
# 관절 수 (FR3/FR5/FR10 등 모두 6축)
num_joints: int = 6
# 관절 이름 (LeRobot 데이터셋에서 사용)
joint_names: list[str] = field(
default_factory=lambda: [
"joint1", "joint2", "joint3",
"joint4", "joint5", "joint6"
]
)
# 관절 제한 (단위: degree)
joint_limits_lower: list[float] = field(
default_factory=lambda: [-175, -265, -150, -265, -175, -175]
)
joint_limits_upper: list[float] = field(
default_factory=lambda: [175, 85, 150, 85, 175, 175]
)
# 제어 주파수 (Hz)
control_hz: float = 20.0
# 움직임 속도 (0.0 ~ 1.0)
move_speed: float = 0.1
# 관절 제어 모식 (0: degree, 1: radian) - LeRobot은 기본 radian 사용
use_radian: bool = True
# 카메라 설정 (선택)
cameras: dict = field(default_factory=dict)
class FaiRobotArm(Robot):
“”"
Fairino FR 로봇 LeRobot 연동 클래스
사용 예시:
config = FaiRobotConfig(robot_ip="192.168.58.2")
robot = FaiRobotArm(config)
robot.connect()
obs = robot.get_observation()
print("현재 관절값:", obs["observation.state"])
action = np.zeros(6)
robot.send_action(action)
robot.disconnect()
"""
def __init__(self, config: FaiRobotConfig):
super().__init__(config)
self.config = config
self._robot: FaiRobot | None = None
self._is_connected = False
# ── 연결 / 해제 ──────────────────────────────
def connect(self) -> None:
"""로봇에 연결합니다."""
print(f"[FR Robot] {self.config.robot_ip} 에 연결 중...")
self._robot = FaiRobot.RPC(self.config.robot_ip)
# 연결 확인 (관절값 읽기로 확인)
ret, joints = self._robot.GetActualJointPosDegree()
if ret != 0:
raise ConnectionError(
f"Fairino 로봇 연결 실패 (error code: {ret})\n"
f"IP 주소({self.config.robot_ip})를 확인해주세요."
)
self._is_connected = True
print(f"[FR Robot] 연결 완료! 현재 관절값(deg): {joints}")
# 카메라 연결
for cam_name, cam_config in self.config.cameras.items():
cam_config.connect()
print(f"[FR Robot] 카메라 '{cam_name}' 연결 완료")
def disconnect(self) -> None:
"""로봇 연결을 해제합니다."""
if self._robot is not None:
# Fairino SDK 연결 종료
self._robot = None
self._is_connected = False
print("[FR Robot] 연결 해제 완료")
# ── 관측값 (Observation) ─────────────────────
def get_observation(self) -> dict[str, Any]:
"""
현재 로봇 상태를 반환합니다.
Returns:
dict with keys:
"observation.state": 관절 위치 (radian, shape: [6])
"observation.images.<cam_name>": 카메라 이미지 (선택)
"""
self._check_connected()
obs = {}
# 현재 관절 위치 읽기 (degree 단위로 반환됨)
ret, joints_deg = self._robot.GetActualJointPosDegree()
if ret != 0:
raise RuntimeError(f"관절값 읽기 실패 (error code: {ret})")
joints = np.array(joints_deg, dtype=np.float32)
# degree → radian 변환 (LeRobot 표준)
if self.config.use_radian:
joints = np.deg2rad(joints)
obs["observation.state"] = joints
# 카메라 이미지
for cam_name, cam in self.config.cameras.items():
img = cam.read()
obs[f"observation.images.{cam_name}"] = img
return obs
# ── 액션 전송 (Action) ───────────────────────
def send_action(self, action: np.ndarray) -> np.ndarray:
"""
관절 위치 명령을 전송합니다.
Args:
action: 목표 관절 위치 (radian, shape: [6])
Returns:
실제 전송된 action (클리핑 후)
"""
self._check_connected()
# radian → degree 변환
if self.config.use_radian:
action_deg = np.rad2deg(action)
else:
action_deg = action.copy()
# 관절 한계 클리핑
lower = np.array(self.config.joint_limits_lower)
upper = np.array(self.config.joint_limits_upper)
action_deg = np.clip(action_deg, lower, upper)
# Fairino SDK: MoveJ (관절 공간 이동)
# 파라미터: joint_pos(list), speed(%), acc(%), tol, ovl, blendT
joint_list = action_deg.tolist()
speed = self.config.move_speed * 100 # 0~100%
ret = self._robot.MoveJ(
joint_list, # 목표 관절 위치 (degree)
speed, # 속도 (%)
speed, # 가속도 (%)
0, # 허용 오차
100, # 오버라이드 (%)
-1, # 블렌딩 시간 (-1: 비활성)
)
if ret != 0:
print(f"[FR Robot] 경고: MoveJ 실패 (error code: {ret})")
return np.deg2rad(action_deg) if self.config.use_radian else action_deg
# ── 유틸 ─────────────────────────────────────
def _check_connected(self):
if not self._is_connected or self._robot is None:
raise RuntimeError("로봇이 연결되어 있지 않습니다. connect()를 먼저 호출하세요.")
@property
def is_connected(self) -> bool:
return self._is_connected
def get_state(self) -> np.ndarray:
"""현재 관절 위치만 반환하는 편의 함수"""
return self.get_observation()["observation.state"]
def move_home(self) -> None:
"""홈 포지션으로 이동"""
home = np.zeros(self.config.num_joints)
self.send_action(home)
time.sleep(2.0)
print("[FR Robot] 홈 포지션 이동 완료")
# ============================================================
# FR (Fairino) 로봇 설정 파일
# 경로: lerobot/configs/robot/fairino_fr.yaml
# ============================================================
robot_type: fairino_fr
# 로봇 연결 설정
robot_ip: "192.168.58.2" # Fairino 로봇 기본 IP
num_joints: 6
move_speed: 0.1 # 속도 비율 (0.0 ~ 1.0)
use_radian: true
# 관절 이름 (데이터셋 저장 키 이름)
joint_names:
- joint1
- joint2
- joint3
- joint4
- joint5
- joint6
# 관절 한계 (degree)
joint_limits_lower: [-175, -265, -150, -265, -175, -175]
joint_limits_upper: [175, 85, 150, 85, 175, 175]
# 카메라 설정 (선택 — 사용 시 주석 해제)
# cameras:
# top:
# _target_: lerobot.common.robot_devices.cameras.opencv.OpenCVCamera
# camera_index: 0
# fps: 30
# width: 640
# height: 480
---
# ============================================================
# UR (Universal Robots) 로봇 설정 파일
# 경로: lerobot/configs/robot/ur_robot.yaml
# ============================================================
robot_type: universal_robots_ur
# 로봇 연결 설정
robot_ip: "192.168.1.100" # UR 로봇 IP (환경에 맞게 변경)
num_joints: 6
rtde_frequency: 50.0 # RTDE 수신 주파수 (Hz)
# 속도 / 가속도
move_speed: 0.5 # rad/s
move_acceleration: 0.5 # rad/s²
# servoJ 파라미터 (실시간 제어 시)
servo_lookahead_time: 0.1
servo_gain: 300.0
# 관절 이름
joint_names:
- shoulder_pan
- shoulder_lift
- elbow
- wrist_1
- wrist_2
- wrist_3
# 관절 한계 (radian, UR 기본값 ±2π)
joint_limits_lower: [-6.28, -6.28, -6.28, -6.28, -6.28, -6.28]
joint_limits_upper: [6.28, 6.28, 6.28, 6.28, 6.28, 6.28]
# 카메라 설정 (선택)
# cameras:
# wrist:
# _target_: lerobot.common.robot_devices.cameras.opencv.OpenCVCamera
# camera_index: 0
# fps: 30
# width: 640
# height: 480
---
# ============================================================
# 사용 예시 (Python 코드)
# ============================================================
## ── FR 로봇 사용 예시 ───────────────────────────────────────
# from fr_robot import FaiRobotArm, FaiRobotConfig
# import numpy as np
#
# # 1. 설정
# config = FaiRobotConfig(robot_ip="192.168.58.2", move_speed=0.1)
# robot = FaiRobotArm(config)
#
# # 2. 연결
# robot.connect()
#
# # 3. 텔레오퍼레이션 / 데이터 수집
# obs = robot.get_observation()
# print("관절값(rad):", obs["observation.state"])
#
# # 4. 액션 전송 (홈 이동)
# action = np.zeros(6)
# robot.send_action(action)
#
# # 5. 연결 해제
# robot.disconnect()
## ── UR 로봇 사용 예시 ───────────────────────────────────────
# from ur_robot import URRobotArm, URRobotConfig
# import numpy as np
#
# config = URRobotConfig(
# robot_ip="192.168.1.100",
# move_speed=0.3,
# move_acceleration=0.3,
# )
# robot = URRobotArm(config)
# robot.connect()
#
# # 현재 상태 읽기
# obs = robot.get_observation()
# print("관절값(rad):", obs["observation.state"])
#
# # 첫 번째 관절 0.1 rad 이동
# action = obs["observation.state"].copy()
# action[0] += 0.1
# robot.send_action(action)
#
# robot.disconnect()
## ── LeRobot CLI 사용 (yaml 설정 사용 시) ──────────────────
# 텔레오퍼레이션:
# lerobot-teleop --robot-path lerobot/configs/robot/fairino_fr.yaml
#
# 데이터 수집:
# lerobot-record \
# --robot-path lerobot/configs/robot/fairino_fr.yaml \
# --repo-id $USER/fr_pick_place \
# --num-episodes 50
#
# 학습:
# lerobot-train \
# --policy=act \
# --dataset.repo_id=$USER/fr_pick_place