제6부 하드웨어 인터페이스
6.5 표준 컨트롤러 카탈로그
6.5.1 이 장에서 배우는 것 / 선수 지식
6.4에서 컨트롤러를 쓰는 법을 봤다. 그런데 쓰기 전에 확인할 것이 있다 — 이미 있는가.
ros2_controllers 저장소는 로봇 제어에서 반복되는 요구를 상당 부분 덮는다. 관절 상태 발행, 위치·속도·토크 전달, 궤적 추종, 차동 구동 오도메트리, 센서 브로드캐스트. 직접 쓴 컨트롤러는 유지보수 부담이고, 표준 컨트롤러는 검증된 자산이다.
이 장은 카탈로그이자 선택 가이드다. 세부 파라미터는 각 프로젝트 문서를 가리키고(BK-04), 여기서는 무엇이 있고 언제 쓰며 무엇을 주의하는가를 다룬다.
배우는 것:
- 브로드캐스터 — 상태를 ROS로 내보내는 것들.
- 전달 컨트롤러 — 명령을 그대로 넘기는 단순 계열.
joint_trajectory_controller— 가장 중요하고 가장 복잡한 것.- 이동 로봇 컨트롤러 — 차동·메카넘·아커만 등.
- 그리퍼·기타 — 특수 목적.
- 선택 기준 — 상황에서 컨트롤러로.
- 설정 패턴 — YAML 구성의 공통 형태.
- Jazzy 실물 확인(6.5.12) — 구독 타입이
TwistStamped로 고정된 것과, 컨트롤러마다 다른 명령 타임아웃.
선수 지식: 6.1~6.4(ros2_control 전반), 5.5(관절 상태·tf), 5.1(오도메트리 프레임).
정직: 컨트롤러 목록·파라미터·기본값은
ros2_controllers버전에 따라 다르다. 아래는 널리 쓰이는 것들의 개관이며 전수가 아니다. 정확한 파라미터는 각 컨트롤러의 문서와generate_parameter_libraryYAML(6.4.6)이 정본이다.실행 확인(2026-08-16): 이 장이 이름을 대는 컨트롤러·브로드캐스터 18종이 전부 실재한다 —
ros-jazzy-ros2-controllers4.40.1을 설치해 플러그인 등록을 기계 대조했다(joint_state_broadcaster/JointStateBroadcaster·joint_trajectory_controller/JointTrajectoryController·diff_drive_controller·ackermann_steering_controller·tricycle_controller·mecanum_drive_controller·admittance_controller·pid_controller·gripper_controller·imu_sensor_broadcaster·force_torque_sensor_broadcaster·range_sensor_broadcaster·pose_broadcaster·forward_command_controller와position_/velocity_/effort_controllers의JointGroup*3종,mock_components/GenericSystem). 이름은 확인됐고, 각 컨트롤러의 파라미터·기본값·동작은 여전히 미검증이다.
6.5.2 분류
| 분류 | 명령 인터페이스 점유 | 대표 |
|---|---|---|
| 브로드캐스터 | ❌ 없음(상태만 읽음) | joint_state_broadcaster |
| 전달 | ✅ | forward_command_controller 계열 |
| 궤적 | ✅ | joint_trajectory_controller |
| 이동 로봇 | ✅ | diff_drive_controller |
| 특수 | ✅ | gripper_controller |
브로드캐스터가 명령을 점유하지 않는다는 점이 중요하다(6.1.5) — 다른 컨트롤러와 항상 함께 쓸 수 있다.
6.5.3 브로드캐스터
6.5.3.1 joint_state_broadcaster
사실상 모든 로봇에 필수다. 상태 인터페이스를 /joint_states로 발행해 robot_state_publisher가 tf를 만들 수 있게 한다(5.5).
joint_state_broadcaster:
ros__parameters:
# 기본: 사용 가능한 모든 관절 (interface_configuration = ALL)
# 특정 관절만 원하면:
# joints: [joint1, joint2]
# interfaces: [position, velocity]
| 발행 | 내용 |
|---|---|
/joint_states | sensor_msgs/JointState(5.5.3) |
/dynamic_joint_states | 표준 외 인터페이스 포함(확인 필요) |
함정: 이것을 먼저 활성화하지 않으면 tf 트리에 가동 관절이 없다(5.5.10). spawner 순서에서 항상 첫 번째다(6.3.6).
6.5.3.2 센서 브로드캐스터
| 컨트롤러 | 발행 | 용도 |
|---|---|---|
imu_sensor_broadcaster | sensor_msgs/Imu | IMU(7.6) |
force_torque_sensor_broadcaster | geometry_msgs/WrenchStamped | 힘토크 센서 |
range_sensor_broadcaster | sensor_msgs/Range | 거리 센서 — 실재(실행 확인) |
pose_broadcaster | geometry_msgs/PoseStamped | 자세 — 실재(실행 확인) |
imu_sensor_broadcaster:
ros__parameters:
sensor_name: imu_sensor
frame_id: imu_link
견해: 센서를 ros2_control의
<sensor>로 넣을지 별도 드라이버 노드로 할지는 판단이 필요하다. 제어 루프에서 그 값을 써야 하면(힘 제어·자세 안정화)<sensor>가 맞다 — 같은 주기로 읽히고 지연이 예측 가능하다. 그렇지 않고 관측·기록용이면 별도 노드가 단순하다.
6.5.4 전달 컨트롤러
명령을 받아 그대로 하드웨어에 넘긴다. 가장 단순하며, 하드웨어가 자체 제어기를 가진 경우에 적합하다.
| 컨트롤러 | 명령 인터페이스 | 입력 토픽 |
|---|---|---|
position_controllers/JointGroupPositionController | position | ~/commands(Float64MultiArray) |
velocity_controllers/JointGroupVelocityController | velocity | 동일 |
effort_controllers/JointGroupEffortController | effort | 동일 |
forward_command_controller/ForwardCommandController | 지정 가능 | 동일 |
position_controller:
ros__parameters:
joints: [joint1, joint2, joint3]
interface_name: position
ros2 topic pub /position_controller/commands std_msgs/msg/Float64MultiArray \
"{data: [0.5, -0.3, 1.0]}"
성질과 한계:
| 성질 | 설명 |
|---|---|
| 단순 | 보간·제한 없이 그대로 전달 |
| 급격한 변화 위험 | 현재 0에서 갑자기 3.0을 명령하면 최대 속도로 이동 |
| 궤적 없음 | 시간에 따른 부드러운 이동 불가 |
| 워치독 없음 | 명령이 끊겨도 마지막 값 유지 — forward_command_controller에 timeout 파라미터가 없다(실행 확인) |
함정: 전달 컨트롤러에 큰 위치 변화를 명령하면 로봇이 최대 속도로 이동한다. 하드웨어에 자체 속도 제한이 없으면 위험하다. 부드러운 이동이 필요하면
joint_trajectory_controller를 쓰라.
견해: 전달 컨트롤러가 맞는 경우는 명확하다 — ① 하드웨어가 자체 궤적·속도 제어를 하는 경우, ② 상위에서 이미 보간된 명령을 고빈도로 보내는 경우(원격 조작·시각 서보), ③ 시험·디버깅. 그 외에는 궤적 컨트롤러가 안전하다.
6.5.5 joint_trajectory_controller
가장 중요한 컨트롤러다. MoveIt 2(9.1)가 이것을 통해 팔을 움직이고, 대부분의 매니퓰레이터 시스템의 중심이다.
하는 일: 시간이 명시된 웨이포인트 궤적을 받아 매 제어 주기마다 보간해 명령을 낸다.
joint_trajectory_controller:
ros__parameters:
joints:
- shoulder_pan
- shoulder_lift
- elbow
command_interfaces:
- position # 하드웨어가 받는 것
state_interfaces:
- position
- velocity
state_publish_rate: 50.0
action_monitor_rate: 20.0
allow_partial_joints_goal: false
open_loop_control: false
constraints:
stopped_velocity_tolerance: 0.01
goal_time: 0.5 # 목표 시간 초과 허용
shoulder_pan:
trajectory: 0.1 # 궤적 추종 중 허용오차
goal: 0.03 # 최종 도달 허용오차
shoulder_lift:
trajectory: 0.1
goal: 0.03
# 하드웨어가 position만 받고 컨트롤러가 PID를 실행해야 하면
gains:
shoulder_pan: {p: 100.0, i: 0.0, d: 10.0, i_clamp: 1.0}
인터페이스 조합에 따라 동작이 다르다:
| command_interfaces | 동작 |
|---|---|
position | 보간된 위치를 그대로 전달(하드웨어가 위치 제어) |
velocity | PID로 위치 오차 → 속도 명령 |
effort | PID로 위치 오차 → 토크 명령 |
position, velocity | 둘 다 전달(하드웨어가 피드포워드로 사용) |
함정:
velocity·effort명령을 쓰면 게인 설정이 필수다. 게인이 0이면 로봇이 움직이지 않고, 너무 크면 진동한다.position명령이면 게인이 불필요하다. 이 차이를 모르고 게인 없이effort를 쓰다 "안 움직인다"로 원인을 찾지 못하는 경우가 흔하다.
허용오차(constraints)가 안전 장치다:
| 항목 | 의미 | 초과 시 |
|---|---|---|
trajectory | 궤적 추종 중 허용 편차 | 궤적 중단 |
goal | 최종 도달 허용 편차 | 목표 실패 보고 |
goal_time | 목표 시간 초과 허용 | 목표 실패 |
stopped_velocity_tolerance | 정지 판정 속도 | 완료 판정 |
견해: 허용오차를 너무 빡빡하게 설정하면 정상 동작에서도 궤적이 중단되고, 너무 느슨하면 충돌해도 계속 간다. 실기에서 실제 추종 오차를 측정해 그 3~5배로 설정하는 것이 출발점이다.
trajectory허용오차는 충돌 감지 역할도 한다 — 팔이 무언가에 부딪히면 오차가 커지고 궤적이 중단된다.
인터페이스:
| 종류 | 이름 | 용도 |
|---|---|---|
| 액션 | ~/follow_joint_trajectory | MoveIt 2가 쓰는 것(1.8) |
| 토픽 | ~/joint_trajectory | 단순 전송(피드백 없음) |
| 토픽 | ~/controller_state | 상태 관측 |
# 액션으로 (권장 — 결과 확인 가능)
ros2 action send_goal /joint_trajectory_controller/follow_joint_trajectory \
control_msgs/action/FollowJointTrajectory "{
trajectory: {
joint_names: [shoulder_pan, shoulder_lift, elbow],
points: [
{positions: [0.5, -0.5, 0.5], time_from_start: {sec: 3}}
]
}
}" --feedback
함정:
time_from_start가 0이거나 너무 짧으면 컨트롤러가 즉시 도달하려 해 급격한 움직임이 된다. 첫 웨이포인트에도 현실적인 시간을 주라.
6.5.6 이동 로봇 컨트롤러
/cmd_vel을 받아 바퀴 명령으로 바꾸고 오도메트리를 발행한다. Nav2(8.1)와 짝을 이룬다.
| 컨트롤러 | 구동 방식 |
|---|---|
diff_drive_controller | 차동 구동(2륜) |
mecanum_drive_controller | 메카넘 휠(전방향) — 실재(실행 확인) |
ackermann_steering_controller | 아커만 조향(자동차형) |
tricycle_controller | 삼륜 |
steering_controllers_library | 조향 계열 공통 기반 — 실재(실행 확인) |
diff_drive_controller:
ros__parameters:
left_wheel_names: [left_wheel_joint]
right_wheel_names: [right_wheel_joint]
wheel_separation: 0.35 # m — 실측 필요
wheel_radius: 0.06 # m — 실측 필요
wheel_separation_multiplier: 1.0
left_wheel_radius_multiplier: 1.0
right_wheel_radius_multiplier: 1.0
publish_rate: 50.0
odom_frame_id: odom # REP 105 (5.1.7)
base_frame_id: base_link
enable_odom_tf: true # odom → base_link 발행
open_loop: false
position_feedback: true
cmd_vel_timeout: 0.5 # ★ 워치독 (1.4.8) — 기본값도 0.5(실행 확인)
# use_stamped_vel 은 Jazzy 의 diff_drive_controller 에 없다 — 6.5.12
linear.x:
has_velocity_limits: true
max_velocity: 1.0
has_acceleration_limits: true
max_acceleration: 1.0
angular.z:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: true
max_acceleration: 2.0
발행하는 것:
| 출력 | 내용 |
|---|---|
/odom | nav_msgs/Odometry |
odom → base_link tf | enable_odom_tf: true일 때(5.1.7) |
함정:
enable_odom_tf를 켠 채로robot_localization(7.6)도 쓰면odom → base_link를 둘이 발행해 충돌한다(5.1.4). 융합 노드를 쓴다면 컨트롤러의 tf 발행을 끄고 오도메트리 토픽만 쓴다.
함정:
wheel_separation·wheel_radius는 반드시 실측하라. CAD 값과 실제가 다르고(타이어 압력·마모), 이 값이 틀리면 오도메트리가 체계적으로 어긋난다. 직선 3 m 주행·제자리 360도 회전을 시켜 보고 보정 배율(*_multiplier)로 맞추는 것이 표준 절차다.
함정: Jazzy의
diff_drive_controller는TwistStamped만 받는다(실행 확인 — 6.5.12). 선택하는 파라미터(use_stamped_vel)가 전혀 없어졌고,Twist를 보내면 구독이 매칭되지 않아 로봇이 움직이지 않는다. 1.2.7에서 예고한 전환 문제가 여기서 실제로 부딪히는 자리다.
cmd_vel_timeout이 안전 장치다 — 명령이 끊기면 정지한다. 1.4.8에서 "명령 토픽에는 워치독 필수"라고 한 것이 여기서 파라미터로 제공된다.
6.5.7 특수 목적
6.5.7.1 gripper_controller
gripper_controller:
ros__parameters:
joint: gripper_joint
goal_tolerance: 0.01
max_effort: 50.0
allow_stalling: true # 물체를 파지해 멈춰도 성공으로
stall_velocity_threshold: 0.001
stall_timeout: 0.5
액션: ~/gripper_cmd(control_msgs/action/GripperCommand)
견해:
allow_stalling이 그리퍼에서 핵심이다. 물체를 파지하면 목표 위치에 도달하지 못한 채 멈추는데, 이것을 실패로 처리하면 파지가 항상 실패한다. 물체를 파지하는 그리퍼에는 반드시 켜야 한다.
6.5.7.2 admittance_controller
힘 정보를 받아 순응 동작을 만든다. 조립·접촉 작업에 쓰이며, 힘토크 센서와 함께 구성한다(확인 필요: 파라미터·성숙도).
6.5.7.3 pid_controller
일반 PID를 인터페이스 사이에 삽입한다. 체이닝(6.4.8)에서 하위 컨트롤러로 쓰이는 경우가 많다(확인 필요).
6.5.8 선택 가이드
상황별 조합:
| 시스템 | 컨트롤러 구성 |
|---|---|
| 이동 로봇 + Nav2 | joint_state_broadcaster + diff_drive_controller |
| 매니퓰레이터 + MoveIt 2 | joint_state_broadcaster + joint_trajectory_controller + gripper_controller |
| 모바일 매니퓰레이터 | 위 둘을 함께 |
| 원격 조작 팔 | joint_state_broadcaster + velocity_controllers |
| 힘 제어 작업 | 위 + force_torque_sensor_broadcaster + admittance_controller |
견해: 직접 쓰기 전에 표준 컨트롤러를 조합으로 해결할 수 있는지 먼저 보라. 특히
joint_trajectory_controller는 기능이 많아(보간·허용오차·부분 목표·액션 인터페이스) 직접 구현하면 상당한 노력이 든다. 표준으로 80%가 되고 20%가 부족하다면, 체이닝(6.4.8)으로 표준 위에 구축하는 것도 방법이다.
6.5.9 설정 패턴
전체 YAML의 표준 구조(6.3.6의 두 단계 구조):
# config/controllers.yaml
controller_manager:
ros__parameters:
update_rate: 500
# ① 컨트롤러 타입 선언
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
imu_broadcaster:
type: imu_sensor_broadcaster/IMUSensorBroadcaster
# ② 각 컨트롤러의 파라미터 (최상위 키)
diff_drive_controller:
ros__parameters:
left_wheel_names: [left_wheel_joint]
right_wheel_names: [right_wheel_joint]
wheel_separation: 0.35
wheel_radius: 0.06
cmd_vel_timeout: 0.5
imu_broadcaster:
ros__parameters:
sensor_name: imu_sensor
frame_id: imu_link
시뮬·실기 설정 분리:
| 접근 | 설명 |
|---|---|
| 단일 YAML | 대부분 동일하므로 권장 |
use_sim_time만 분기 | launch에서 주입 |
| 게인만 분리 | 시뮬과 실기의 동역학 차이(10.6) |
견해: 컨트롤러 설정은 가능한 한 하나로 유지하라 — 5.6.5의 단일 출처 원칙이 여기도 적용된다. 게인처럼 정말 달라야 하는 것만 별도 파일로 오버레이한다. 관절 이름·인터페이스 구성이 시뮬과 실기에서 같다면(5.6.6) 설정 대부분이 공유된다.
launch 구성:
control_node = Node(
package='controller_manager', executable='ros2_control_node',
parameters=[robot_description, controllers_yaml],
output='both',
)
spawners = []
for name in ['joint_state_broadcaster', 'imu_broadcaster', 'diff_drive_controller']:
spawners.append(Node(
package='controller_manager', executable='spawner',
arguments=[name, '--controller-manager', '/controller_manager'],
))
# 순서 보장은 OnProcessExit 연쇄로 (6.3.6)
6.5.12 Jazzy의 컨트롤러 실물 — 실측
이 장이 나열한 컨트롤러·브로드캐스터가 Jazzy에 실제로 있는지, 그리고 파라미터가 책과 맞는지 확인했다(실행 확인, ros-jazzy-ros2-controllers).
6.5.12.1 diff_drive_controller는 TwistStamped만 받는다
이 장에서 가장 중요한 정정이다. 책은 use_stamped_vel 파라미터로 Twist와 TwistStamped를 고른다고 적었는데, Jazzy에는 그 파라미터가 없다.
라이브러리 심볼을 보면 구독 타입이 하나뿐이다(실행 확인):
geometry_msgs::msg::TwistStamped_ ... DiffDriveController::on_configure
Twist 쪽 심볼은 나오지 않는다. 선택지가 사라지고 TwistStamped로 고정된 것이다.
증상이 까다롭다. Twist를 발행하는 쪽(옛 텔레옵 노드, 직접 만든 조이스틱 브리지, 일부 Nav2 구성)과 연결하면 타입이 달라 구독이 전혀 매칭되지 않는다 — 에러도 경고도 없이 로봇이 아무 표시 없이 안 움직인다. 1.2.7이 예고한 Twist → TwistStamped 전환이 실제로 부딪히는 자리다.
진단:
ros2 topic info /diff_drive_controller/cmd_vel -v로 구독자 쪽 타입을 보라. 발행자가geometry_msgs/msg/Twist, 구독자가TwistStamped면 이 경우다. 해결은 발행 쪽을TwistStamped로 바꾸거나 변환 노드를 하나 두는 것이다.
견해:
TwistStamped로 간 방향 자체는 옳다. 속도 명령에 스탬프와frame_id가 없다는 것이 오래된 설계 결함이었다(1.2.7). 다만 전환기에는 두 타입이 섞여 돌아다니므로, 시스템을 조립할 때 어느 쪽 타입인지를 먼저 확인하는 습관이 필요하다.
6.5.12.2 나열한 컨트롤러는 전부 실재한다
(확인 필요)가 붙어 있던 것들을 포함해 확인했다(실행 확인):
| 확인한 것 | 결과 |
|---|---|
range_sensor_broadcaster · pose_broadcaster | 실재 |
mecanum_drive_controller · steering_controllers_library | 실재 |
admittance_controller · pid_controller | 실재 |
joint_state_broadcaster · diff_drive_controller | 실재 |
6.3.6의 5차 검증에서 컨트롤러 18종 전부 실재를 이미 확인했고, 이번에 (확인 필요)가 남아 있던 나머지도 같은 결과다.
6.5.12.3 워치독은 컨트롤러마다 다르다
(확인 필요)였던 "전달 컨트롤러에 워치독 없음"도 확인했다 — 맞다.
| 컨트롤러 | 명령 타임아웃 |
|---|---|
diff_drive_controller | cmd_vel_timeout = 0.5초(기본값, 실행 확인) |
forward_command_controller | 없다 — 파라미터에 timeout이 전혀 없다(실행 확인) |
이 차이가 안전에 직결된다. 이동 로봇은 명령이 끊기면 0.5초 뒤 멈추지만, 전달 컨트롤러로 관절을 제어하면 명령이 끊겨도 마지막 값을 유지한다 — 팔이 그 자세로 힘을 계속 쓴다는 뜻이다.
함정: "ros2_control이 자동으로 안전 정지를 해 준다"고 가정하지 마라. 컨트롤러가 제공할 때만 있다. 전달 컨트롤러를 쓴다면 상위에서 워치독을 직접 만들거나(1.4.8·14.2.12), 타임아웃이 있는 컨트롤러로 바꿔야 한다.
6.5.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | joint_state_broadcaster 미활성 | tf에 가동 관절 없음 | 항상 먼저 spawn |
| 2 | 전달 컨트롤러에 큰 위치 변화 | 최대 속도로 이동 | 궤적 컨트롤러 |
| 3 | effort·velocity 명령에 게인 없음 | 로봇이 안 움직임 | gains 설정(6.5.5) |
| 4 | 허용오차가 너무 빡빡 | 정상 동작에서 궤적 중단 | 실측 오차의 3~5배 |
| 5 | 허용오차가 너무 느슨 | 충돌해도 계속 진행 | 균형 |
| 6 | time_from_start가 0 | 급격한 움직임 | 현실적 시간 |
| 7 | wheel_separation·radius 미실측 | 오도메트리 체계적 오차 | 주행 시험·보정 |
| 8 | enable_odom_tf + 융합 노드 | tf 충돌(5.1.4) | 하나만 발행 |
| 9 | cmd_vel_timeout 미설정 | 통신 끊겨도 계속 주행 | 워치독 필수 |
| 10 | Twist/TwistStamped 불일치 | 명령이 안 먹음 | 타입 확인(1.2.7) |
| 11 | 그리퍼 allow_stalling 미설정 | 파지가 항상 실패 | 켜기 |
| 12 | YAML 두 단계 구조 혼동 | 파라미터 미적용 | 6.3.6 |
| 13 | 표준 확인 없이 직접 작성 | 불필요한 유지보수 | 카탈로그 먼저 |
| 14 | Twist를 보냈는데 로봇이 안 움직임 | Jazzy는 TwistStamped만 받는다 | 6.5.12.1 |
6.5.11 체크포인트
- 컨트롤러 분류 다섯 가지와, 브로드캐스터가 명령을 점유하지 않는다는 사실의 의미를 말할 수 있는가.
joint_state_broadcaster를 항상 먼저 활성화해야 하는 이유를 5.5와 연결해 설명할 수 있는가.- 센서를
<sensor>로 넣을지 별도 노드로 할지의 판단 기준은? - 전달 컨트롤러의 위험과, 그것이 맞는 세 가지 경우를 말할 수 있는가.
joint_trajectory_controller의 command_interface 조합에 따른 동작 차이를 말할 수 있는가. 게인이 필요한 경우는?- 허용오차 세 종류와 각각의 초과 시 동작은?
trajectory허용오차의 부수적 역할은? - 이동 로봇 컨트롤러가 발행하는 것 두 가지와, 융합 노드와 함께 쓸 때의 주의점은?
wheel_separation·wheel_radius보정 절차를 말할 수 있는가.cmd_vel_timeout이 구현하는 원칙은 무엇인가(1.4.8).- 그리퍼에서
allow_stalling이 핵심인 이유는? - 시스템 유형별 컨트롤러 조합을 네 가지 이상 말할 수 있는가.
- 표준으로 80%만 되는 경우의 접근은?
- Jazzy의
diff_drive_controller는 어떤 타입을 구독하는가. 전달 컨트롤러에 명령 타임아웃이 있는가(6.5.12).
다음 장 6.6은 지금까지 반복해 온 "실시간 제약"을 정면으로 다룬다 — ros2_control의 제어 루프가 실제로 무엇을 보장하고 무엇을 보장하지 않는가, 그리고 realtime_tools의 정확한 사용법.