이제 물체를 집을 그리퍼가 필요하다. 상용 그리퍼의 URDF를 가져다 적용할 수도 있겠지만 단순히 물체를 잡는 목적이라면 URDF 작성법에 익숙해질 겸해서 직접 작성하여 사용하는 것이 더 빠를 것으로 생각되어 이 방향으로 진행한다.
tool0을 부모 링크로 하여 상자 모양 손가락 두 개를 prismatic 관절(일직선으로 미끄러지는 선형 관절)로 붙인다.
src/arm_description/urdf/arm_gripper.urdf.xacro
핵심적인 설정 내용은 다음과 같다.
axis를 Y축을 기준으로 서로 반대로 설정. 같은 방향이면 관절 값을 증가시킬 때 두 손가락이 나란히 한쪽으로 밀린다. 반대로 줘야 중심을 향해 모인다.<limit> 태그 속성을 lower=0.0, upper=0.025로 지정한다.finger2_joint에 <mimic> 속성을 부여. 평행 그리퍼는 두 축이 물리적으로 연동되므로 한쪽만 제어하면 된다. 조인트에 mimic 속성을 주어 finger1_joint가 움직인 만큼 axis 속성에 따라 움직이도록 만든다.물체를 잡는 지점으로 쓸 tool center point인 tcp_link도 고정 관절(fixed joint)로 연결하여 추가한다. tool0은 UR 로봇의 공구 장착면이고, 손가락 관절 원점이 거기서 z=0.05, 손가락 길이가 0.05이므로 그리퍼의 중심 좌표의 z축 위치는 z=0.075 m다. tcp_link를 그 위치에 둔다.
이렇게 설계한 그리퍼가 파지할 테스트 대상물로 두께 5 mm, 가로 20 mm, 세로 40 mm 크기의 직육면체를 가정했다. 손가락이 맞닿는 가로 폭(20 mm)을 그리퍼의 최대 개방 폭(50 mm)보다 충분히 좁게 설정하여 안정적인 파지 여유를 두었다.
UR 로봇의 ROS 2 패키지인 ur_description 내부의 매크로를 참고하여, 그리퍼용 매크로를 같은 형태로 하나 더 만든다.
src/arm_description/urdf/gripper_joint_control.xacro
finger2_joint에는 <command_interface>를 두지 않는다. mimic joint는 명령을 받는 대상이 아니라 상태만 보고하는 대상이다. 여기에 command_interface를 달면 관절을 움직이는 주체가 둘이 되어 동작에 문제가 발생한다. gz_ros2_control의 GazeboSimSystem 플러그인은 URDF 내의 조인트에 <mimic> 태그가 있으면 이를 읽어서 mimic 쌍으로 등록한다.
생성한 gripper_joint_control.xacro 매크로를 arm_study.ros2_control.xacro에서 포함시키고 시뮬레이션용 Gazebo 플러그인을 하드웨어로 설정한다.
<xacro:include filename="$(find arm_description)/urdf/gripper_joint_control.xacro"/>
처음에는 그리퍼가 목표 위치로 이동해야 하므로 finger1_joint를 팔의 joint_trajectory_controller에 넣어서 MoveIt이 tcp_link의 위치를 계산해야 한다고 생각했다. 하지만, 살펴보니 tcp_link는 tool0에 fixed 조인트로 부착된 링크이며, MoveIt의 계획 그룹인 ur_manipulator에는 UR 로봇의 6축 관절만 포함되어 있어 그리퍼는 제외된다.
또한, 하나의 command interface는 한 컨트롤러만 점유할 수 있다. joint_trajectory_controller가 finger1_joint의 position 명령을 점유하고 있으면 그리퍼를 여닫기 위한 그리퍼용 컨트롤러가 해당 인터페이스를 제어할 수 없다.
(tcp_link를 목표 위치로 설정하는 것과 관련된 내용은 다음에 다시 정리하도록 하겠다.)
우선, 그리퍼 전용 컨트롤러를 새로 만들어 준다.
src/arm_bringup/config/arm_controllers.yaml
controller_manager:
ros__parameters:
gripper_controller:
type: parallel_gripper_action_controller/GripperActionController
gripper_controller:
ros__parameters:
joint: finger1_joint
allow_stalling: true # 물체를 붙잡아 목표까지 못 가도 성공으로 처리
stall_velocity_threshold: 0.001
stall_timeout: 0.5
goal_tolerance: 0.002
joint에는 finger1_joint만 적는다. finger2_joint는 mimic이라 컨트롤러가 직접 다루지 않는다.
allow_stalling: true가 핵심이다. 물체를 잡으면 손가락이 목표 위치까지 가지 못한 채 멈추는데, 이를 실패가 아닌 성공으로 처리하라는 뜻이다. 물체를 잡았을 때 성공 신호는 stalled=true가 된다.
형상 확인용으로 joint_state_publisher_gui + RViz 런치를 따로 만들었는데 로봇이 제대로 그려지지 않았다.

이미지에서 보이듯이 RobotModel의 상태가 'Error'로 표시되고 "No transform from [//base] to [base_link]"처럼 모든 링크에 대한 TF를 받지 못하고 있는 상태이다.
원인은 URDF가 아니라 RViz 설정 파일 문제였다. RobotModel 디스플레이의 TF Prefix가 /로 남아 있어 모든 링크 이름 앞에 /를 붙여 TF를 찾다가 실패하고 있었다. 좌측 Display에서 TF Prefix 항목의 '/'를 삭제하면 정상적으로 나타난다.
실행 시 모든 관절에서 중복 등록 에러가 발생했다.
ResourceStorage: ... already existing key
arm_study.ros2_control.xacro를 ur5e.urdf.xacro와 arm_gripper.urdf.xacro 양쪽에서
include 하고 있었다. 이 파일의 <ros2_control> 블록은 매크로로 감싸여 있지 않아 include 횟수만큼 그대로 펼쳐진다. 그리퍼 관절만이 아니라 모든 관절이 두 번씩 등록된 것이다. 매크로가 아닌 xacro 파일은 include 지점을 한 곳으로 유지해야 한다.
<mimic> 태그를 달고 follower의 인터페이스도 정리한 상태에서, gz_ros2_control은 mimic 쌍을 인식했다는 로그를 정확히 출력했다.
Joint 'finger2_joint' is mimicking joint 'finger1_joint' with multiplier: 1 and offset: 0
그런데 Gazebo에서 finger1_joint를 움직여도 finger2_joint는 따라오지 않았다. 인식은 하고 있으니 URDF나 ros2_control 설정의 문제는 아니었다.
원인은 물리 엔진이었다. gz_ros2_control은 mimic 관계를 파악한 뒤, 두 관절을 묶어 달라고 물리 엔진에 제약(constraint) 생성을 요청한다. 관절을 실제로 연동시키는 주체는 ROS 계층이 아니라 물리 엔진인 것이다. 그런데 Gazebo의 기본 엔진인 dartsim은 이 제약을 구현하고 있지 않다.
같은 증상이 업스트림에도 보고되어 있다 —
Error mimic_joints [ROS Jazzy + Gazebo Harmonic] #340.
다행히 bullet-featherstone이라는 시뮬레이션 엔진이 이를 구현하고 있고 Gazebo에 이미 포함되어 있어서, 런치의 gz_args에 엔진을 변경하여 지정하면 되었다.
src/arm_bringup/launch/arm_study_bringup.launch.py
"gz_args": IfElseSubstitution(
gazebo_gui,
if_value=[" -r -v 4 --physics-engine gz-physics-bullet-featherstone-plugin ", world_file],
else_value=[" -s -r -v 4 --physics-engine gz-physics-bullet-featherstone-plugin ", world_file],
)
이 변경으로 두 손가락이 대칭으로 움직이기 시작했다.
====== 2026.10 변경 사항 =======
이 당시에는 물리 엔진 교체로 모든 문제가 해결된 것으로 보았으나, 반복 테스트에서 그리퍼가 오동작하는 경우가 계속 발생하였다. 원인은 제어기 명령과 물리 엔진 제약이 서로 충돌하면서 발생하는 오류였다. 이 내용은 뒤에 나올 「Mimic Joint 동작 문제」 글에서 다룬다.