AEGLOSLAB 제어공학 종합 핸드북

제7부 실전: ROS2 Humble 프로젝트

7.1 ROS2 제어 아키텍처

제7부 실전: ROS2 Humble 프로젝트 · 시작. 지금까지의 이론(제1~6부)을 실제 로봇에 탑재하는 기반 — ROS2 Humble의 구조를 제어 관점에서 본다. 노드·토픽·서비스, 제어용 표준 인터페이스, 실행 모델·실시간, 그리고 ros2_control 프레임워크. 3.3의 제어 루프가 ROS2 노드가 되는 다리.

✅ 검증 환경: 이 부의 명령·코드는 ros:humble 컨테이너(Ubuntu 22.04·Python 3.10)에서 실제 실행해 검증했다(저장소 verify/). 단 GUI(turtlesim·Gazebo·rviz)·실물 하드웨어는 headless 한계로 독자 환경 몫 — 해당 부분은 명시한다.

이 장을 마치면 ROS2의 통신 모델을 제어에 어떻게 쓰는지 설명하고, 워크스페이스에 제어 노드를 만들며, ros2_control의 역할을 이해해야 한다.

7.1.1 왜 ROS2 — 분산 로봇 미들웨어

로봇은 센서·액추에이터·계획·인지가 여러 프로세스·여러 컴퓨터에 흩어진 분산 시스템이다. ROS2는 이들을 잇는 미들웨어로:

  • 노드(node) 단위로 기능을 나누고, DDS(Data Distribution Service)로 노드 간 통신.
  • 표준 메시지·도구·시뮬레이터(Gazebo)·라이브러리(Nav2·MoveIt·ros2_control) 생태계.
  • 언어 무관(C++ rclcpp, Python rclpy), 분산·실시간 지향(ROS1 대비 개선).

제어 관점: 센서 측정과 액추에이터 명령이 토픽으로 흐르고, 제어기는 그 사이의 노드다. 0.1.4의 블록선도가 노드·토픽 그래프가 된다.

7.1.2 핵심 개념 — 노드·토픽·서비스·액션

통신성격제어에서
토픽(topic)비동기 스트림, pub/sub, 다대다주력 — 센서값·명령의 연속 흐름(/odom, /cmd_vel)
서비스(service)동기 요청-응답, 1회성설정·질의(게인 변경, 리셋)
액션(action)장시간 작업 + 피드백·취소궤적 실행, "거기까지 가라"
파라미터(parameter)노드 설정값게인 $K_p,K_i,K_d$ 등

제어 루프는 거의 토픽으로 구성한다: 측정 토픽을 구독하고, 명령 토픽을 발행한다. 메시지 타입(std_msgs, geometry_msgs 등)이 데이터 형식을 정한다.

7.1.3 제어에 쓰는 표준 인터페이스

바퀴를 다시 발명하지 말고 표준 메시지를 쓴다(검증: 컨테이너에서 import 확인).

메시지용도
geometry_msgs/Twist속도 명령 /cmd_vel(선속도 linear·각속도 angular) — 이동로봇 입력
nav_msgs/Odometry/odom — 위치·자세·속도 추정(출력 측정)
sensor_msgs/ImuIMU(각속도·가속도·자세) — 5.4 융합 입력
sensor_msgs/JointState관절 위치·속도·노력 — 로봇팔·바퀴
control_msgs/*ros2_control 컨트롤러 인터페이스

좌표 변환은 $tf2$(프레임 트리, 5.4.7)가 표준. 이 인터페이스 덕에 제어기를 플랫폼 독립적으로 작성할 수 있다.

7.1.4 실행 모델과 실시간

  • 콜백·실행기(executor): 노드는 콜백(타이머·구독)으로 동작하고, 실행기가 콜백을 스케줄링한다. SingleThreaded(순차)·MultiThreaded(병렬, 콜백 그룹으로 제어).
  • 타이머: 고정 주기 제어 루프는 create_timer(dt, cb)로(3.3.3의 on_timer).
  • ⚠️ 실시간: ROS2는 기본적으로 경성 실시간이 아니다(3.3.9). 기본 실행기·DDS·OS 스케줄러는 지터를 허용한다. 엄격한 주기가 필요하면:
    • rclcpp(C++) + 실시간 커널(PREEMPT_RT) + 실시간 실행기,
    • QoS 설정(신뢰성·이력·마감), 메모리 사전할당,
    • 또는 빠른 루프는 ros2_control(전용 실시간 업데이트 루프, 다음 절)에 맡긴다.
    • rclpy(Python)는 편리하지만 GC·GIL로 경성 실시간에는 부적합 — 학습·상위 제어엔 충분.

7.1.5 ros2_control 프레임워크

매 프로젝트마다 모터 드라이버·실시간 루프를 새로 작성하는 것은 낭비다. ros2_control이 하드웨어 추상화 + 교체 가능한 컨트롤러 + 실시간 루프를 표준화한다.

controller_manager (실시간 업데이트 루프) command/state interfaces (claim) Controllers diff_drive · joint_trajectory · forward · (내 PID...) hardware_interface command_interfaces(쓰기) · state_interfaces(읽기) 실물 HW 또는 시뮬레이션 (gazebo_ros2_control / mock)
  • controller_manager: 일정 주기로 read()(센서)→update()(컨트롤러)→write()(액추에이터) 루프를 반복 수행한다(3.3 루프의 표준판).
  • hardware_interface: 하드웨어를 command/state 인터페이스로 추상화 — 같은 컨트롤러를 실물·시뮬레이션·다른 로봇에 재사용.
  • 표준 인터페이스 타입: 관절·축마다 인터페이스는 position(위치 명령/측정)·velocity(속도)·effort(힘·토크) 중 필요한 것을 이름으로 지정해 claim한다 — 예: 위치 서보는 position, 바퀴 속도 제어는 velocity, 힘/토크로 구동하는 제어(7.4의 카트-폴 $F$ 등)는 effort. 컨트롤러가 어떤 타입을 요청하느냐가 곧 그 컨트롤러가 하드웨어와 주고받는 물리량을 정한다.
  • 컨트롤러: diff_drive_controller(차동구동, 7.3)·joint_trajectory_controller·forward_command_controller 등 기성품 + 직접 만든 컨트롤러 플러그인.

권장: 표준 거동(차동구동 등)은 기성 컨트롤러를, 학습·커스텀 제어는 직접 노드(7.2)나 커스텀 ros2_control 컨트롤러를. 이 책은 원리 학습을 위해 직접 노드로 시작하고, ros2_control은 실전 통합에서 쓴다.

7.1.5.1 컨트롤러 생명주기 — "컨트롤러가 활성화되지 않는다"의 원인

ros2_control의 컨트롤러(그리고 하드웨어 컴포넌트)는 관리형 생명주기(managed lifecycle, ROS2 rclcpp_lifecycle 표준)의 상태기계를 따른다. 로드됐다고 바로 동작하는 것이 아니다 — 이 단계를 모르면 "컨트롤러를 스폰했는데 왜 동작하지 않나"가 현장에서 가장 흔한 혼란 요인이다.

로드(load_ controller) configure() activate() shutdown() cleanup() deactivate() Unconfigured Inactive Active Finalized
  • Unconfigured: 컨트롤러가 로드만 됐다. 파라미터도 안 읽었고, 하드웨어 인터페이스도 아직 안 잡음(claim 안 함).
  • configure() → Inactive: 파라미터를 읽고(YAML), 필요한 command_interfaces/state_interfaces를 요청한다. 아직 실행은 안 함 — 준비만.
  • activate() → Active: 인터페이스를 실제로 claim하고, controller_manager의 실시간 update() 루프에 들어가 매 주기 실제로 실행된다. 이 상태여야 액추에이터가 움직인다.
  • deactivate() → Inactive: 인터페이스 claim을 놓는다(다른 컨트롤러가 같은 인터페이스를 쓸 수 있게).
  • cleanup() → Unconfigured, shutdown() → Finalized: 자원 반환·종료.

controller_manager가 이 상태 전이를 관장하고, 컨트롤러 스포너(spawner)(ros2 run controller_manager spawner <이름>)는 관례상 로드→configure→activate를 한 번에 해 준다 — 그래서 보통은 상태기계가 안 보이지만, 실패하면 바로 이 단계 중 하나에서 멈춘 것이다(파라미터 오타로 configure 실패 → Unconfigured에 갇힘, 등).

CLI로 직접 들여다보고 조작한다:

ros2 control list_controllers            # 이름·타입·상태(active/inactive/unconfigured) 확인 — 1차 진단
ros2 control list_hardware_interfaces    # 어떤 command/state 인터페이스가 누구에게 claim됐는지
ros2 control load_controller my_ctrl     # 로드만 (Unconfigured)
ros2 control set_controller_state my_ctrl inactive   # configure (Unconfigured→Inactive)
ros2 control set_controller_state my_ctrl active     # activate  (Inactive→Active)

ros2 control switch_controllers는 같은 하드웨어 인터페이스를 두고 컨트롤러를 핫스왑한다 — 예: 평소에는 diff_drive_controller가 바퀴 조인트를 claim해 동작하다가, 정밀 위치 제어가 필요할 때 joint_trajectory_controller로 전환한다:

ros2 control switch_controllers --deactivate diff_drive_controller --activate joint_trajectory_controller

두 컨트롤러가 같은 command_interfaces를 요구하면 동시에 Active일 수 없다(인터페이스 claim 충돌) — switch_controllers가 한쪽을 비활성화하고 한쪽을 활성화하는 것을 원자적으로 처리해 준다. 흔한 함정: 이 전환 없이 두 번째 컨트롤러를 단순히 activate하면 인터페이스 claim 실패로 에러가 나거나(둘 다 원하는데 하나만 가짐), 반대로 전환은 됐는데 옛 컨트롤러가 Inactive로 남아 "분명 뗐는데 아직 반응한다"고 착각하는 경우다 — list_controllers로 상태를 항상 확인하는 습관이 최선의 디버깅이다.

⚠️ 정직: 이 절의 CLI·상태기계는 ROS2 Humble의 ros2_control 표준 동작에 근거해 정확히 서술했으나, 이 책의 컨테이너 검증은 GUI·ros2_control 전체 스택까지는 다루지 않는다(이 장 서두에 밝힌 headless 한계 밖 + 학습 곡선상 이 책은 직접 노드를 우선). 실제 명령 동작은 여러분의 ros2_control 설치 환경에서 확인하라.

7.1.6 워크스페이스와 패키지 (검증됨)

0.3.8의 colcon 흐름을 컨테이너에서 검증한 그대로:

source /opt/ros/humble/setup.bash
mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0 ce_examples   # 패키지 생성
cd ~/ros2_ws
colcon build --packages-select ce_examples        # 빌드  → Finished <<< ce_examples
source install/setup.bash                          # overlay 소싱
ros2 pkg list | grep ce_examples                   # → ce_examples (확인)

✅ 위 전 과정(생성→빌드→소싱→확인)을 ros:humble에서 실행해 통과 확인. (Humble에서 --license를 생략하면 경고만 나고 동작은 함 — 명시 권장.)

7.1.7 제어 루프를 ROS2로 — 3.3이 노드가 된다

3.3.8의 매핑을 코드로. 최소 제어 노드 골격:

import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64        # 예시 메시지 (실제로는 Twist·JointState 등)
# PID 클래스는 3.3.5 참조 구현(전문은 7.2.2에 인라인)

class ControllerNode(Node):
    def __init__(self):
        super().__init__('controller')
        self.r = 3.0                                       # 목표
        self.pid = PID(Kp=2.0, Ki=4.0, Kd=0.0, dt=0.02, u_min=-10.0, u_max=10.0)  # ★ 3.3 PID 초기화(게인은 플랜트에 맞게)
        self.pub = self.create_publisher(Float64, 'motor_cmd', 10)      # 명령 발행
        self.create_subscription(Float64, 'motor_speed', self.on_meas, 10)  # 측정 구독
        # (고정 주기가 필요하면 self.create_timer(dt, self.loop) 사용)
    def on_meas(self, msg):
        y = msg.data                                       # ① 측정
        u = self.pid.update(self.r, y)                     # ② 제어(3.3 PID)
        m = Float64(); m.data = u; self.pub.publish(m)     # ③ 구동(명령 발행)

def main():
    rclpy.init(); rclpy.spin(ControllerNode()); rclpy.shutdown()

read_sensor→on_meas 콜백, write_actuator→publish, pid.update는 3.3 그대로. 이 패턴으로 7.2에서 실제 폐루프를 실행해 목표 수렴까지 검증한다(컨테이너 PASS).

7.1.8 정직 — 실시간·동기·복잡도

  • 경성 실시간 아님(기본): 빠른·안전필수 루프는 rclcpp+실시간 설정 또는 ros2_control로(7.1.4).
  • 시간 동기·QoS: 센서 타임스탬프·DDS QoS(신뢰성·이력) 설정이 성능을 좌우. 잘못된 QoS는 메시지 유실·지연을 부른다.
  • ros2_control 학습곡선: 강력하나 URDF·설정(YAML)·플러그인 구조가 가팔라 입문에는 부담 — 이 책은 직접 노드로 원리를 먼저 익힌다.
  • GUI·실물은 별도: turtlesim·Gazebo·실로봇은 이 책의 컨테이너 검증 밖(headless 한계). 노드 통신·연산은 검증, 시각·실구동은 독자 환경에서.

7.1.9 정리 — 그리고 다음 장

  • ROS2는 노드를 토픽(스트림)·서비스·액션으로 잇는 분산 미들웨어. 제어 루프는 측정 토픽 구독 + 명령 토픽 발행으로 구성한다(7.1.2·7.1.7).
  • 제어에는 표준 인터페이스(Twist·Odometry·Imu·JointState·$tf2$)를 쓴다(7.1.3).
  • 실행은 콜백·실행기·타이머로, 경성 실시간은 기본이 아님 → rclcpp/실시간 설정/ros2_control(7.1.4). ros2_control이 하드웨어 추상화 + 실시간 루프 + 교체 가능 컨트롤러를 표준화(7.1.5).
  • 워크스페이스 흐름(생성·빌드·소싱)과 노드 골격을 검증했다(7.1.6·7.1.7).

다음 7.2에서는 이 골격으로 DC모터/turtlesim 속도·위치 PID를 실제 폐루프로 구현하고 목표 수렴을 검증한다 — 첫 실전 프로젝트.

7.1.10 연습·생각해 볼 것

  1. 통신 고르기. 다음에 토픽·서비스·액션 중 무엇이 맞나: ⓐ 50 Hz 바퀴 속도 명령, ⓑ "PID 게인을 리셋하라", ⓒ "(x,y)까지 주행하라(진행률 보고)". (7.1.2)
  2. 표준 인터페이스. 차동구동 로봇의 입력과 출력 측정에 각각 어떤 표준 메시지를 쓰겠나? IMU 융합에는? (7.1.3)
  3. 실시간. 왜 ROS2 기본 rclpy 타이머가 경성 실시간을 보장 못 하나? 1 kHz 토크 루프가 필요하면 어떤 선택지가 있나? (7.1.4)
  4. ros2_control. controller_manager의 read→update→write 루프가 3.3의 제어 루프와 어떻게 대응하나? hardware_interface 추상화의 이점은? (7.1.5)
  5. 노드 매핑. 3.3.5의 PID 클래스를 ROS2 노드로 만들 때 read_sensor·write_actuator·on_timer가 각각 무엇이 되나? (7.1.7)
  6. (손으로) verify/의 흐름대로 ce_examples 패키지를 만들고 빌드·소싱해 ros2 pkg list에 나타나는지 확인하라. 컨테이너가 없으면 0.3 환경에서. (7.1.6)

막히면 "측정 토픽 구독 + 명령 토픽 발행 = 제어 노드, 빠른 루프는 ros2_control·rclcpp, 표준 메시지를 써라"로 돌아가라.