AEGLOSLAB ROS 2 Jazzy 대전

제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_library YAML(6.4.6)이 정본이다.

실행 확인(2026-08-16): 이 장이 이름을 대는 컨트롤러·브로드캐스터 18종이 전부 실재한다 — ros-jazzy-ros2-controllers 4.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 분류

표준 컨트롤러 브로드캐스터 상태 → ROS 전달 컨트롤러 명령 → 하드웨어 궤적 컨트롤러 시간 궤적 추종 이동 로봇 속도 → 바퀴 + 오도메트리 특수 목적 그리퍼 · 어드미턴스
분류명령 인터페이스 점유대표
브로드캐스터❌ 없음(상태만 읽음)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_statessensor_msgs/JointState(5.5.3)
/dynamic_joint_states표준 외 인터페이스 포함(확인 필요)

함정: 이것을 먼저 활성화하지 않으면 tf 트리에 가동 관절이 없다(5.5.10). spawner 순서에서 항상 첫 번째다(6.3.6).

6.5.3.2 센서 브로드캐스터

컨트롤러발행용도
imu_sensor_broadcastersensor_msgs/ImuIMU(7.6)
force_torque_sensor_broadcastergeometry_msgs/WrenchStamped힘토크 센서
range_sensor_broadcastersensor_msgs/Range거리 센서 — 실재(실행 확인)
pose_broadcastergeometry_msgs/PoseStamped자세 — 실재(실행 확인)
imu_sensor_broadcaster:
  ros__parameters:
    sensor_name: imu_sensor
    frame_id: imu_link

견해: 센서를 ros2_control의 <sensor>로 넣을지 별도 드라이버 노드로 할지는 판단이 필요하다. 제어 루프에서 그 값을 써야 하면(힘 제어·자세 안정화) <sensor>가 맞다 — 같은 주기로 읽히고 지연이 예측 가능하다. 그렇지 않고 관측·기록용이면 별도 노드가 단순하다.

6.5.4 전달 컨트롤러

명령을 받아 그대로 하드웨어에 넘긴다. 가장 단순하며, 하드웨어가 자체 제어기를 가진 경우에 적합하다.

컨트롤러명령 인터페이스입력 토픽
position_controllers/JointGroupPositionControllerposition~/commands(Float64MultiArray)
velocity_controllers/JointGroupVelocityControllervelocity동일
effort_controllers/JointGroupEffortControllereffort동일
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)가 이것을 통해 팔을 움직이고, 대부분의 매니퓰레이터 시스템의 중심이다.

하는 일: 시간이 명시된 웨이포인트 궤적을 받아 매 제어 주기마다 보간해 명령을 낸다.

FollowJointTrajectory 액션 또는 토픽 궤적 수신 웨이포인트 + 시간 매 주기 보간 현재 시각의 목표값 계산 (선택) PID 보정 command_interface 허용오차 검사 초과 시 중단
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보간된 위치를 그대로 전달(하드웨어가 위치 제어)
velocityPID로 위치 오차 → 속도 명령
effortPID로 위치 오차 → 토크 명령
position, velocity둘 다 전달(하드웨어가 피드포워드로 사용)

함정: velocity·effort 명령을 쓰면 게인 설정이 필수다. 게인이 0이면 로봇이 움직이지 않고, 너무 크면 진동한다. position 명령이면 게인이 불필요하다. 이 차이를 모르고 게인 없이 effort를 쓰다 "안 움직인다"로 원인을 찾지 못하는 경우가 흔하다.

허용오차(constraints)가 안전 장치다:

항목의미초과 시
trajectory궤적 추종 중 허용 편차궤적 중단
goal최종 도달 허용 편차목표 실패 보고
goal_time목표 시간 초과 허용목표 실패
stopped_velocity_tolerance정지 판정 속도완료 판정

견해: 허용오차를 너무 빡빡하게 설정하면 정상 동작에서도 궤적이 중단되고, 너무 느슨하면 충돌해도 계속 간다. 실기에서 실제 추종 오차를 측정해 그 3~5배로 설정하는 것이 출발점이다. trajectory 허용오차는 충돌 감지 역할도 한다 — 팔이 무언가에 부딪히면 오차가 커지고 궤적이 중단된다.

인터페이스:

종류이름용도
액션~/follow_joint_trajectoryMoveIt 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

발행하는 것:

출력내용
/odomnav_msgs/Odometry
odom → base_link tfenable_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 선택 가이드

예 아니오 아니오 예 아니오 예 예 무엇이 필요한가? 상태를 ROS로 내보내야 하나? joint_ state_ broadcaster + 센서 브로드캐스터 이동 로봇인가? 구동 방식별 diff_drive 등 시간 궤적을 따라가야 하나? joint_ trajectory_ controller 하드웨어가 자체 제어를 하나? forward_ command 계열 직접 작성 검토 6.4

상황별 조합:

시스템컨트롤러 구성
이동 로봇 + Nav2joint_state_broadcaster + diff_drive_controller
매니퓰레이터 + MoveIt 2joint_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_controllercmd_vel_timeout = 0.5초(기본값, 실행 확인)
forward_command_controller없다 — 파라미터에 timeout이 전혀 없다(실행 확인)

이 차이가 안전에 직결된다. 이동 로봇은 명령이 끊기면 0.5초 뒤 멈추지만, 전달 컨트롤러로 관절을 제어하면 명령이 끊겨도 마지막 값을 유지한다 — 팔이 그 자세로 힘을 계속 쓴다는 뜻이다.

함정: "ros2_control이 자동으로 안전 정지를 해 준다"고 가정하지 마라. 컨트롤러가 제공할 때만 있다. 전달 컨트롤러를 쓴다면 상위에서 워치독을 직접 만들거나(1.4.8·14.2.12), 타임아웃이 있는 컨트롤러로 바꿔야 한다.

6.5.10 함정 카탈로그

#함정증상대처
1joint_state_broadcaster 미활성tf에 가동 관절 없음항상 먼저 spawn
2전달 컨트롤러에 큰 위치 변화최대 속도로 이동궤적 컨트롤러
3effort·velocity 명령에 게인 없음로봇이 안 움직임gains 설정(6.5.5)
4허용오차가 너무 빡빡정상 동작에서 궤적 중단실측 오차의 3~5배
5허용오차가 너무 느슨충돌해도 계속 진행균형
6time_from_start가 0급격한 움직임현실적 시간
7wheel_separation·radius 미실측오도메트리 체계적 오차주행 시험·보정
8enable_odom_tf + 융합 노드tf 충돌(5.1.4)하나만 발행
9cmd_vel_timeout 미설정통신 끊겨도 계속 주행워치독 필수
10Twist/TwistStamped 불일치명령이 안 먹음타입 확인(1.2.7)
11그리퍼 allow_stalling 미설정파지가 항상 실패켜기
12YAML 두 단계 구조 혼동파라미터 미적용6.3.6
13표준 확인 없이 직접 작성불필요한 유지보수카탈로그 먼저
14Twist를 보냈는데 로봇이 안 움직임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의 정확한 사용법.