제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, Pythonrclpy), 분산·실시간 지향(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/Imu | IMU(각속도·가속도·자세) — 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: 일정 주기로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 표준)의 상태기계를 따른다. 로드됐다고 바로 동작하는 것이 아니다 — 이 단계를 모르면 "컨트롤러를 스폰했는데 왜 동작하지 않나"가 현장에서 가장 흔한 혼란 요인이다.
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 연습·생각해 볼 것
- 통신 고르기. 다음에 토픽·서비스·액션 중 무엇이 맞나: ⓐ 50 Hz 바퀴 속도 명령, ⓑ "PID 게인을 리셋하라", ⓒ "(x,y)까지 주행하라(진행률 보고)". (7.1.2)
- 표준 인터페이스. 차동구동 로봇의 입력과 출력 측정에 각각 어떤 표준 메시지를 쓰겠나? IMU 융합에는? (7.1.3)
- 실시간. 왜 ROS2 기본
rclpy타이머가 경성 실시간을 보장 못 하나? 1 kHz 토크 루프가 필요하면 어떤 선택지가 있나? (7.1.4) - ros2_control.
controller_manager의read→update→write루프가 3.3의 제어 루프와 어떻게 대응하나?hardware_interface추상화의 이점은? (7.1.5) - 노드 매핑. 3.3.5의 PID 클래스를 ROS2 노드로 만들 때
read_sensor·write_actuator·on_timer가 각각 무엇이 되나? (7.1.7) - (손으로)
verify/의 흐름대로ce_examples패키지를 만들고 빌드·소싱해ros2 pkg list에 나타나는지 확인하라. 컨테이너가 없으면 0.3 환경에서. (7.1.6)
막히면 "측정 토픽 구독 + 명령 토픽 발행 = 제어 노드, 빠른 루프는 ros2_control·rclcpp, 표준 메시지를 써라"로 돌아가라.