ROS 2 실전 교재v7.35

ROS 2 실전 교재#

Jazzy · ROS 2 개발 · TF2 · SLAM · Nav2 · E0509 · AI·RAG·Agent

이 책은 ROS 2의 기본 개념과 설치부터 CLI, Python 중심의 Client Library, 중급 개발 기법, TF2·상태추정·SLAM·Nav2·MoveIt2, 그리고 DDS/RMW·성능·테스트·배포·AI Agent까지 한 흐름으로 학습하도록 구성한다. 명령과 예제 코드는 실행 방법뿐 아니라 각 옵션과 API가 맡는 역할까지 함께 설명한다.

이 책을 어떻게 사용할 것인가#

학습 순서는 일곱 단계다. ① ROS 2가 무엇인지 개념을 잡는다. ② Jazzy를 설치하고 CLI로 실행 중 시스템을 관찰한다. ③ Python의 rclpy로 package와 Node를 직접 작성한다. ④ ROS 2 시스템을 재사용 가능하게 구성하는 중급 개발기법을 익힌다. ⑤ 좌표·모델·상태추정과 시뮬레이션으로 로봇 시스템을 구성한다. ⑥ SLAM·Nav2·MoveIt2·비전으로 자율로봇 기능을 통합한다. ⑦ DDS/RMW·성능·테스트·배포·AI Agent까지 전문가 수준으로 확장한다.

독학 규칙 명령을 복사해 실행하는 것에서 끝내지 않는다. 어떤 Node가 생겼는가, 어떤 Topic이 흐르는가, frame은 무엇인가, 누가 상태를 가지고 있는가, 실패하면 어느 계층부터 볼 것인가를 매번 답한다.

명령과 예제 코드를 읽는 최소 규칙#

이 책은 Python의 기본 문법 자체를 반복 설명하지 않는다. 다만 Python을 알아도 바로 뜻을 알기 어려운 ROS 2 API, Linux shell 문법, 빌드 도구, 네트워크·시간·좌표계 개념은 첫 등장 때 충분히 설명하고, 뒤의 관련 실습과 단원 정리에서 두세 차례 다른 예시로 다시 풀어 쓴다. 같은 용어가 다시 등장하면 그 실습에서 무엇을 의미하고 왜 필요한지 확인한다.

~ 현재 Linux 사용자의 홈 디렉터리. 예: ~/ros2_ws

$NAME shell 환경변수의 값을 참조. 예: $ROS_DOMAIN_ID

\ shell 명령을 다음 줄까지 이어 쓰는 line continuation

| 앞 명령의 표준출력을 뒤 명령의 표준입력으로 넘기는 pipe

# shell/Python에서 주석. 같은 기호라도 실행 환경은 별개

package / executable ros2 run에서 앞은 패키지, 뒤는 설치된 실행 진입점

name:=value ros2 launch argument 전달 형식

--ros-args 이 뒤부터 ROS 전용 CLI 인자라는 경계

단계 독자가 할 일 도달 수준
0. 개념 About ROS·Node·Graph·Interface·Parameter ROS 2가 제공하는 역할과 통신모델을 설명
1. 설치·CLI Jazzy 설치·환경설정·turtlesim·ros2 CLI·rqt·rosbag2 실행 중 ROS Graph를 직접 관찰·조작
2. Client Library colcon·workspace·package·Python pub/sub·service/client·interface 작은 ROS 2 package를 Python으로 구현
3. Intermediate rosdep·Action·Composition·NodeInterfaces·Parameter monitoring·Launch·tf2·Testing·URDF·RViz 재사용 가능한 ROS 2 애플리케이션 구조를 설계
4. 로봇 시스템 Gazebo·ros2_control·TF2·URDF·Odometry·EKF 로봇의 공간·상태·하드웨어 경계를 일관되게 구성
5. 자율·통합 SLAM Toolbox·AMCL·Nav2·MoveIt2·E0509·Hand-Eye 지도·주행·조작·비전을 하나의 시스템으로 연결
6. 전문가·AI Executor·DDS/RMW·composition·tracing·CI·RAG·Agent 성능·확장성·운영·AI 통합까지 설계

목차#

Part 0. About ROS 2 — 공식 입문 개념을 한 번에 이해하기

0장. About ROS — ROS 2가 무엇이며 무엇을 제공하는가

Part I. Installation — ROS 2 Jazzy 설치와 환경 준비

1장. Installation 선택지와 이 교재의 기준 환경

2장. Ubuntu 24.04에 ROS 2 Jazzy 설치

Part II. Tutorials — Beginner: CLI tools

3장. Configuring environment — shell과 ROS 환경

4장. Using turtlesim, ros2, and rqt

5장. Understanding nodes

6장. Understanding topics

7장. Understanding services

8장. Understanding parameters

9장. Understanding actions

10장. rqt_console로 로그 보기

11장. Launching multiple nodes

12장. rosbag2로 데이터 기록·재생

Part III. Tutorials — Beginner: Client libraries

13장. colcon으로 ROS 2 package 빌드하기

14장. Workspace와 Package 만들기

15장. Publisher와 Subscriber — Python

16장. Service와 Client — Python

17장. Custom msg·srv·action Interface

18장. Python Parameter·ros2doctor

Part III-A. Intermediate ROS 2 개발

Part IV. Concepts & Development Deep Dive

19장. ROS 2를 분산 시스템으로 이해하기

20장. Package 구조·빌드 체계·개발환경 진단

21장. Node·Topic·Message·QoS·Custom Interface

22장. Service·Action·Parameter·Launch·Lifecycle

23장. Topic·Service·Action·Launch·Remapping을 직접 구현하기

Part V. 로봇을 움직이며 배우기

24장. Jazzy 시뮬레이터와 이동로봇을 ROS Graph로 읽기

25장. 실습 프로젝트 — LaserScan + FSM 장애물 회피

Part VI. 공간·로봇 모델·상태추정

26장. TF2와 SE(2)/SE(3): 좌표와 시간을 함께 다루기

27장. URDF·Xacro·robot_state_publisher·RViz2

28장. Gazebo·ros2_control·시뮬레이션과 실제 하드웨어의 경계

29장. Twist·차동구동·Odometry

30장. 센서·공분산·Kalman Filter·robot_localization

31장. 실습 프로젝트 — TF2 장애물 검출기와 RViz Marker

32장. 실습 프로젝트 — VFF 반응형 회피·rosbag2·테스트

Part VII. SLAM과 Nav2

33장. Occupancy Grid·Scan Matching·Pose Graph·Loop Closure

34장. TurtleBot3 Cartographer와 지도 작성

35장. AMCL과 지도 기반 Localization

36장. Nav2 아키텍처·Costmap·Planner·Controller

37장. Behavior Tree·Lifecycle·Bringup·튜닝

38장. Nav2 Python API와 실전 시나리오

39장. 실습 프로젝트 — Behavior Tree를 Nav2 행동으로 연결하기

40장. 실습 프로젝트 — Nav2 목표 이동·피드백·취소·순찰

41장. 실습 프로젝트 — TurtleBot3 Cartographer SLAM에서 Nav2까지

Part VIII. 로봇팔·비전 통합

42장. MoveIt2·Planning Scene·E0509

43장. Hand-Eye·3D 비전·Pick 파이프라인

Part IX. 재사용 가능한 ROS 2 시스템

44장. 실습 프로젝트 — Custom Interface + Lifecycle Node

45장. 남이 실행할 수 있는 ROS 2 패키지로 마감하기

Part X. 전문가 ROS 2 엔지니어링

46장. Executor·Callback Group·시간·동시성

47장. rosbag2·로그·네트워크·WSL2·성능 진단

48장. 통합 설계 원칙과 종합 실습

49장. 실습 프로젝트 — Executor·Callback Group·QoS 실패 실험

50장. DDS/RMW·Discovery·QoS 요청/제공 모델

51장. Composition·Intra-process·프로세스 경계 설계

52장. Tracing·Latency·Throughput·메모리·성능 측정

53장. Multi-machine·Multi-robot·Clock·Namespace·Network

54장. Plugin Architecture — Nav2·ros2_control을 확장하는 방법

55장. 테스트·CI·버전·배포·품질 선언

Part XI. 전문가 사례연구

사례연구 1. 하나의 ROS 2 패키지로 Topic·Service·Action·Parameter·Launch를 모두 연결하기

사례연구 2. TF2·URDF를 이용해 센서 좌표를 로봇 좌표로 바꾸기

사례연구 3. 차동구동 Odometry를 계산하고 보정하기

사례연구 4. rosbag 하나로 SLAM 문제를 반복 재현하기

사례연구 5. Nav2가 “안 간다”를 15분 안에 분해하는 절차

사례연구 6. 비전 기반 Pick을 단계별로 검증하기

사례연구 7. 로봇 프로젝트를 “데모”가 아니라 검증 가능한 시스템으로 마무리하기

Part XII. 로보틱스를 위한 AI·LLM·RAG·Agent

56장. 로봇 개발자가 알아야 할 LLM 핵심 개념

57장. 프롬프트 엔지니어링: 자연어를 안정적인 인터페이스로 만들기

58장. LLM API·Structured Output·Tool Calling

59장. Open Source·Local LLM과 Ollama

60장. RAG 기초: 로봇 매뉴얼을 검색 가능한 지식으로 만들기

61장. Advanced RAG: 검색 품질을 단계적으로 높이기

62장. LangChain·LlamaIndex를 구성요소로 이해하기

63장. Autonomous Agent와 Tool-using Robotics

64장. AI 페어 프로그래밍을 ROS 2 개발에 제대로 사용하기

65장. 통합 프로젝트 — 자연어 명령을 검증된 ROS 2 Action으로 연결하기

부록 A. Ubuntu 24.04 + ROS 2 Jazzy 설치와 핵심 CLI

부록 B. 최소 패키지 템플릿

부록 C. 로봇공학 수식 치트시트

부록 D. 확인문제 해설

부록 E. 공식 참고자료

부록 F. 실습 코드 구조와 자료

부록 G. 기술 참고자료

부록 H. AI·LLM·RAG 실무 치트시트

부록 I. ROS 2 Jazzy 전문 레퍼런스

I.1 Node 실행 인자·Remapping·Parameter·Logging 운용

I.2 QoS 전체 모델과 rosbag2 QoS Override

I.3 관찰가능성: Logging·Topic Statistics·ros2 doctor

I.4 SROS2와 DDS Security: 로봇 네트워크를 신뢰경계로 설계하기

I.5 TF2 심화: 시간·Stamped Data·MessageFilter

I.6 Domain ID·RMW·Discovery Server·Multi-machine 운영

I.7 ROS 2 내부 인터페이스와 Zero-copy 성능 최적화

Part 0. About ROS 2 — 공식 입문 개념을 한 번에 이해하기#

0장. About ROS — ROS 2가 무엇이며 무엇을 제공하는가#

이 장에서는 ROS 2의 역할과 핵심 구성요소를 먼저 정리한다. 이후 설치와 실습에서 반복해서 등장하는 Node, Graph, Interface, Parameter, Client Library, Discovery를 여기서 기준 개념으로 잡는다.

약어 표기 ROS: Robot Operating System(로봇 운영체제) · ROS 2: Robot Operating System 2(로봇 운영체제 2)

0.1 ROS는 전통적인 운영체제가 아니다#

ROS(Robot Operating System)는 로봇 애플리케이션을 만들고 배포하고 실행하고 유지보수하기 위한 오픈소스 생태계다. 이름에는 Operating System이 들어가지만 Linux나 Windows처럼 CPU·메모리·파일시스템을 직접 관리하는 커널형 운영체제가 아니다. 여러 로봇 프로그램이 서로 통신하고 재사용 가능한 소프트웨어로 구성되도록 framework, tools, libraries, 표준 interface를 제공한다.

실제 로봇에서는 카메라 driver, LiDAR driver, localization, perception, planner, controller, UI가 서로 다른 프로세스·언어·컴퓨터에서 실행될 수 있다. ROS 2는 이들을 하나의 ROS Graph 안에서 발견하고 연결하며, 데이터를 주고받는 공통 규칙을 제공한다.

0.2 ROS 생태계의 세 축: Framework, Tools, Capabilities#

구분 역할 대표 예
Framework 프로그램 간 통신과 실행 구조 Node, Topic, Service, Action, Parameter, client library
Tools 개발·검사·시각화·기록 ros2 CLI, rqt, RViz2, rosbag2, launch
Capabilities 로봇 기능을 구현하는 package 생태계 tf2, URDF, Navigation2, MoveIt2, ros2_control

Framework는 로봇 기능 자체가 아니라 기능들을 연결하는 배관(plumbing)이다. Tools는 동작을 관찰하고 구성한다. Capabilities는 그 위에서 실제 localization, navigation, manipulation 같은 로봇 기능을 제공한다.

0.3 Node는 ROS Graph의 기본 실행 단위다#

Node는 ROS 2 Graph에 참여하는 논리적 계산 단위이며 일반적으로 한 가지 책임을 맡는다. 하나의 Node는 동시에 Publisher, Subscriber, Service Server/Client, Action Server/Client, Parameter를 가질 수 있다. Node는 같은 프로세스 안에 있을 수도 있고 다른 프로세스·다른 컴퓨터에 있을 수도 있다.

Node와 executable은 다르다 executable은 운영체제가 실행하는 프로그램 파일이고 Node는 ROS Graph의 논리적 참가자다. 하나의 executable이 여러 Node를 포함할 수 있다.

0.4 Interfaces: Topic, Service, Action#

Node는 실행되는 프로그램의 이름과 같지 않다. 하나의 프로세스에서 여러 Node를 실행할 수도 있으므로 ros2 node list는 ROS Graph의 논리 노드를 보여준다.

Interface 통신 패턴 적합한 작업 취소
Topic 비동기 Publish/Subscribe 센서 스트림, 상태, 명령 없음
Service Request/Response 짧고 즉시 끝나는 질의·설정 없음
Action Goal/Feedback/Result 시간이 걸리는 이동·작업 가능

Topic은 지속적인 데이터 스트림에 적합하다. Service는 요청과 응답이 한 쌍이며 짧은 작업에 적합하다. Action은 장시간 작업을 goal·feedback·result로 다루며 cancel을 지원한다. 각 interface는 .msg, .srv, .action 정의로 데이터 구조를 기술하고 ROS가 Python에서 사용할 수 있는 메시지 타입 코드를 생성한다.

0.5 Message·Service·Action 정의 파일#

# .msg
float32 temperature
string frame_name

# .srv
string mode
---
bool accepted
string message

# .action
geometry_msgs/PoseStamped goal
---
bool success
---
float32 progress

msg는 field 목록, srv는 요청과 응답을 ---로 구분하고, action은 goal·result·feedback 세 부분을 두 개의 ---로 나눈다. field에는 정수·실수·문자열·배열·다른 ROS message 타입을 사용할 수 있다.

0.6 Parameter는 Node별 설정값이다#

Parameter는 각 Node가 보유하는 런타임 설정값이다. Topic처럼 계속 흘러가는 데이터가 아니라 해당 Node의 동작을 조절하는 설정에 가깝다. 예를 들어 카메라 exposure, controller gain, 최대속도, map frame 이름 등을 parameter로 둘 수 있다.

0.7 Client Library: Python의 rclpy#

ROS 2의 Node는 통신에 필요한 API(Application Programming Interface, 응용 프로그램 인터페이스)를 제공하는 Client Library를 이용해 작성한다. 이 책의 Python Client Library는 rclpy다. rclpy는 Node 생성, Publisher와 Subscription 등록, Service·Action 호출, Parameter 관리, Executor 실행을 제공한다. ROS Graph의 Topic·Service·Action은 특정 언어에 묶이지 않으므로, 먼저 Python 예제로 동작을 익힌다.

0.8 Discovery와 분산 Graph#

ROS 2 Node는 중앙 master 하나에 등록하는 방식이 아니라 middleware의 discovery를 통해 서로를 찾는다. 따라서 같은 네트워크·도메인에서 실행되는 Node들이 자동으로 Graph를 구성할 수 있다. 이후 QoS와 DDS/RMW를 배우면 이 discovery와 실제 전송 계층을 더 깊게 다룬다.

0.9 ROS 2 Jazzy Jalisco#

ROS 2는 배포판(distribution) 단위로 릴리스된다. Jazzy Jalisco는 Ubuntu 24.04(Noble)를 주요 Ubuntu 플랫폼으로 사용하는 장기지원 계열의 ROS 2 배포판이다. package 이름에는 ros-jazzy- 접두가 붙고 설치 위치는 일반적으로 /opt/ros/jazzy다.

Part I. Installation — ROS 2 Jazzy 설치와 환경 준비#

1장. Installation 선택지와 이 교재의 기준 환경#

교재 기준 Windows 11 + WSL2(Windows Subsystem for Linux 2, Windows용 Linux 하위 시스템 2) Ubuntu 24.04 또는 Ubuntu 24.04 네이티브 환경에서 ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy Desktop deb 패키지를 설치하는 흐름을 주 기준으로 한다.

1.1 설치 방식 선택#

방식 언제 선택하는가 특징
Ubuntu deb package 대부분의 Ubuntu 사용자 권장. apt가 dependency와 업데이트 관리
Windows binary Windows 네이티브가 필요한 경우 binary archive와 별도 의존성 설정
RHEL RPM RHEL 기반 환경 RPM 패키지 사용
Source build ROS 2 core 수정·특수 빌드 시간과 dependency 관리가 더 필요
Container 환경 격리·CI GUI·USB·네트워크 연결을 추가 고려

이 교재에서는 일반 사용·개발 목적이므로 Ubuntu deb package를 선택한다. source build는 ROS 2 core 자체를 수정하거나 바이너리가 없는 플랫폼을 지원해야 할 때 검토한다.

1.2 Desktop과 ROS(Robot Operating System, 로봇 운영체제)-Base#

ros-jazzy-desktop은 core 통신 기능뿐 아니라 RViz, demo, tutorial 등 GUI와 학습도구를 포함한다. ros-jazzy-ros-base는 통신 라이브러리·message·CLI 중심의 최소 설치로 GUI 도구가 없다. 이 교재는 tutorial과 RViz를 사용하므로 Desktop을 권장한다.

1.3 Ubuntu 24.04(Noble) 확인#

lsb_release -a
uname -m

lsb_release는 Ubuntu 배포판 정보를, uname -m은 CPU(Central Processing Unit, 중앙처리장치) architecture를 확인한다. Jazzy의 Ubuntu 패키지는 Noble 24.04를 기준으로 제공된다.

1.4 Ubuntu 실행 환경 준비하기#

Ubuntu는 PC(Personal Computer, 개인용 컴퓨터)에 직접 설치하거나 Windows 안의 WSL2·가상머신에서 실행할 수 있다. 직접 설치하면 운영체제가 장치에 바로 접근하고, 듀얼부팅은 재부팅할 때 Windows와 Ubuntu 중 하나를 선택한다. 가상머신은 Windows를 실행한 채 가상의 PC에서 Ubuntu를 구동한다. WSL2는 Windows 안에서 Linux 개발환경을 제공한다. 터미널·패키지 실습은 어느 방식으로도 시작할 수 있으며, GUI(Graphical User Interface, 그래픽 사용자 인터페이스)와 외부 장치 연결은 선택한 환경에서 따로 확인한다.

PC에 직접 설치할 때는 Ubuntu 24.04 Desktop의 CPU에 맞는 ISO 이미지를 준비한다. ISO는 설치 디스크 전체를 담은 파일이다. 일반적인 Intel·AMD 64비트 PC는 amd64 이미지를 사용한다. Rufus에서 설치용 USB(Universal Serial Bus, 범용 직렬 버스)와 ISO를 선택하고 UEFI(Unified Extensible Firmware Interface, 통합 확장 펌웨어 인터페이스) PC에 맞춰 GPT 파티션 방식을 지정한다. UEFI는 PC의 부팅 펌웨어 규격이며, GPT는 디스크 파티션 정보를 저장하는 방식이다. USB 기록이 완료되면 PC의 부팅 메뉴에서 해당 USB의 UEFI 항목을 선택한다.

Windows와 함께 설치하려면 먼저 기존 데이터를 백업하고 Windows 디스크 관리에서 볼륨을 축소해 할당되지 않은 공간을 확보한다. Ubuntu 설치기의 Install Ubuntu alongside Windows Boot Manager는 Windows와 함께 설치하는 선택지다. Erase disk and install Ubuntu는 선택한 디스크를 지우는 선택지이므로 듀얼부팅 단계에서 서로 구분해야 한다. 설치 전에 최종 디스크 구성 화면에서 Windows 파티션과 Ubuntu에 배정할 공간을 확인한다.

BitLocker 또는 장치 암호화가 감지되면 Windows에서 복구 키와 암호화 상태를 확인한 뒤 설치기가 요구하는 절차를 따른다. 부팅 순서 설정은 PC별로 메뉴와 키가 다르다. 설치가 끝나면 USB를 제거하고 GRUB 메뉴에서 Ubuntu와 Windows가 각각 부팅되는지 확인한다. GRUB은 부팅할 운영체제를 고르는 부트로더다.

가상머신으로 실습할 때는 VirtualBox에 새 Linux 가상머신을 만들고 Ubuntu 24.04 ISO를 가상 광학 드라이브에 연결해 설치한다. 가상머신 안의 디스크는 PC의 실제 Windows 파티션과 구분된다. Ubuntu 화면에서 터미널을 열고 아래 명령으로 설치된 운영체제를 확인한 뒤 ROS 2 설치로 진행한다.

cat /etc/os-release
python3 --version

cat은 파일 내용을 터미널에 출력한다. /etc/os-release의 VERSION_ID가 24.04인지 확인한다. python3 --version은 실행 가능한 Python 3의 버전을 보여준다. 가상머신·WSL2·직접 설치 모두 ROS 2 Jazzy를 설치하는 Linux 쪽에서 이 명령을 실행한다.

2장. Ubuntu 24.04에 ROS 2 Jazzy 설치#

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

2.1 UTF-8 Locale 확인#

locale
sudo apt update && sudo apt install -y locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
locale

ROS(Robot Operating System, 로봇 운영체제) 도구와 Python package가 파일명·로그 문자열을 일관되게 처리하도록 UTF-8 locale을 사용한다. export는 현재 shell과 자식 프로세스가 볼 환경변수를 설정한다.

2.2 Universe repository 활성화#

sudo apt install -y software-properties-common
sudo add-apt-repository universe

Ubuntu Universe repository에는 ROS dependency로 사용되는 여러 package가 포함된다. add-apt-repository는 apt source 구성을 관리하는 명령이다.

2.3 ROS 2 apt source 등록#

sudo apt update && sudo apt install -y curl
export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F'"' '{print $4}')
curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"
sudo dpkg -i /tmp/ros2-apt-source.deb

ros2-apt-source package는 ROS package repository 주소와 서명 key 구성을 설치한다. curl은 파일을 내려받고 dpkg -i는 .deb 파일을 설치한다. 명령이 길어 보이지만 목적은 현재 Ubuntu codename에 맞는 ROS repository 설정 package를 내려받아 설치하는 것이다.

2.4 개발 도구와 ROS 2 Desktop 설치#

sudo apt update
sudo apt upgrade
sudo apt install -y ros-dev-tools
sudo apt install -y ros-jazzy-desktop

ros-dev-tools에는 colcon, rosdep 및 개발에 자주 필요한 도구 묶음이 포함된다. ros-jazzy-desktop은 ROS 2 Jazzy Desktop variant다.

2.5 환경 활성화#

source /opt/ros/jazzy/setup.bash
printenv ROS_DISTRO

source는 setup.bash를 현재 shell에 적용한다. ROS_DISTRO가 jazzy라면 해당 터미널은 Jazzy 환경을 보고 있다. 새 터미널을 열면 다시 source해야 한다.

2.6 설치 검증: Python talker와 listener#

# Terminal A
source /opt/ros/jazzy/setup.bash
ros2 run demo_nodes_py talker

# Terminal B
source /opt/ros/jazzy/setup.bash
ros2 run demo_nodes_py listener

Python talker가 std_msgs/msg/String 메시지를 Topic으로 발행(Publish)하고 Python listener가 구독(Subscribe)해 같은 번호의 메시지를 받으면 Node 실행·Discovery·Topic 전송이 정상 동작한다. 두 프로그램은 직접 서로를 호출하는 것이 아니라 이름과 메시지 타입이 일치하는 Topic을 통해 연결된다.

정상 결과: talker에는 Publishing: Hello World..., listener에는 I heard: Hello World...가 반복된다. 각 프로세스를 다른 터미널에서 실행하므로 터미널별로 ROS 환경을 source해야 한다.

2.7 자동 source 설정#

echo 'source /opt/ros/jazzy/setup.bash' >> ~/.bashrc
source ~/.bashrc

.bashrc는 Bash가 새 interactive shell을 열 때 읽는 설정 파일이다. >>는 기존 파일 끝에 한 줄을 추가한다. 같은 source 줄을 여러 번 중복 추가하지 않도록 주의한다.

2.8 제거·문제해결 원칙#

apt 설치 문제는 OS version, apt repository, package index, 네트워크와 mirror 상태를 먼저 확인한다. package가 일부 설치되지 않는다고 바로 build from source로 우회하지 말고 apt update와 repository 설정을 점검한다. 이 교재 뒤의 진단 장에서 ros2 doctor와 환경변수 점검을 더 자세히 다룬다.

Part II. Tutorials — Beginner: CLI tools#

3장. Configuring environment — shell과 ROS 환경#

약어 표기 ROS: Robot Operating System(로봇 운영체제)

3.1 setup.bash의 역할#

ROS package를 설치해도 shell의 PATH·PYTHONPATH·AMENT_PREFIX_PATH 같은 검색경로가 자동으로 바뀌지는 않는다. setup.bash는 현재 shell이 /opt/ros/jazzy 아래 executable, Python module, package resource를 찾도록 환경변수를 추가한다.

source /opt/ros/jazzy/setup.bash
printenv | grep -i ROS
which ros2

which ros2는 shell이 실제로 실행할 ros2 CLI(Command-Line Interface, 명령줄 인터페이스) 위치를 보여준다. 여러 ROS 배포판이나 workspace를 source했을 때 어떤 환경을 보고 있는지 확인하는 기본 진단이다.

3.2 환경변수와 ROS_DOMAIN_ID#

echo $ROS_DISTRO
echo $ROS_DOMAIN_ID
export ROS_DOMAIN_ID=10

ROS_DOMAIN_ID는 discovery domain을 분리한다. 같은 네트워크에서도 domain ID가 다르면 기본적으로 서로 발견하지 않는다. 여러 교육생이 같은 LAN(Local Area Network, 근거리 통신망)에서 같은 Topic 이름을 사용할 때 충돌을 줄일 수 있다.

3.2.1 팀별 도메인과 localhost 범위 확인#

ROS_DOMAIN_ID를 설정하지 않으면 기본 도메인 0을 사용한다. 이 값은 같은 네트워크에서 서로 발견할 ROS 2(Robot Operating System 2, 로봇 운영체제 2) participant 그룹을 나누지만, 암호화나 접근 제어를 제공하는 보안 경계는 아니다. ROS_LOCALHOST_ONLY=1은 discovery와 통신 범위를 현재 PC(Personal Computer, 개인용 컴퓨터)로 제한한다. 두 환경변수는 이미 실행 중인 노드에 소급 적용되지 않으므로 값을 바꾼 뒤 관련 노드를 종료하고 새 터미널에서 다시 실행한다.

먼저 같은 LAN의 두 PC에서 두 변수를 해제한다. echo가 빈 줄이면 설정되지 않은 상태이며, ROS_DOMAIN_ID는 기본값 0으로 동작한다.

unset ROS_DOMAIN_ID ROS_LOCALHOST_ONLY
echo $ROS_DOMAIN_ID
echo $ROS_LOCALHOST_ONLY

송신 PC에서 talker를, 수신 PC에서 listener를 실행한다. 같은 기본 도메인과 호환되는 DDS(Data Distribution Service, 데이터 분산 서비스) 설정을 사용하면 다른 PC의 메시지를 받을 수 있다.

# 송신 PC
ros2 run demo_nodes_py talker

# 수신 PC
ros2 run demo_nodes_py listener

같은 공간에서 여러 팀이 실습할 때는 팀별로 도메인 값을 다르게 지정한다. 아래 명령은 각 팀의 모든 송신·수신 터미널에 동일하게 적용한다. 1팀이 10, 2팀이 20을 사용하면 같은 팀 안에서는 발견되지만 팀 사이에서는 노드와 토픽이 보이지 않는다.

# 1팀의 모든 ROS 2 터미널
export ROS_DOMAIN_ID=10

# 2팀의 모든 ROS 2 터미널
export ROS_DOMAIN_ID=20

한 PC 안에서만 통신하도록 제한하려면 송신·수신 터미널 모두에서 ROS_LOCALHOST_ONLY를 켠다. 같은 도메인을 쓰더라도 다른 PC의 listener는 메시지를 받지 못하고, 송신 PC에서 새로 실행한 listener만 메시지를 받는다. 실제 로봇과 제어 PC처럼 서로 다른 장치가 통신해야 하는 구성에서는 이 값을 해제해야 한다.

export ROS_LOCALHOST_ONLY=1
echo $ROS_LOCALHOST_ONLY

# 실습을 마친 뒤 원래 범위로 복귀
unset ROS_DOMAIN_ID ROS_LOCALHOST_ONLY

통신 결과를 확인할 때는 각 터미널에서 echo로 환경변수를 먼저 비교하고, ros2 node list와 ros2 topic list로 discovery 결과를 확인한다. 도메인 값이 같아도 방화벽, 멀티캐스트 차단, 서로 다른 RMW(ROS Middleware Interface, ROS 미들웨어 인터페이스) 구현의 설정 때문에 통신하지 못할 수 있으므로 ROS_DOMAIN_ID만 반복 변경하며 원인을 추정하지 않는다.

3.3 현재 경로와 파일 구조 읽기#

셸은 명령을 입력하고 프로그램을 실행하는 환경이다. 터미널마다 현재 작업 디렉터리가 하나씩 있으며, 상대 경로는 그 위치를 기준으로 해석된다. Python 코드가 올바르더라도 다른 폴더에서 실행하면 파일을 찾지 못할 수 있다. 명령을 실행하기 전에 pwd로 현재 위치를 확인하는 습관을 들인다.

pwd
ls
ls -a
ls -l
cd ~
mkdir -p ~/ros2_shell_practice/robot_basic
cd ~/ros2_shell_practice
touch robot_basic/hello_ros.py
ls -l robot_basic
표기 또는 명령의미와 이 실습에서의 역할
pwd현재 작업 디렉터리의 절대 경로를 출력한다.
ls / ls -a / ls -l파일 목록 / 숨김 항목 포함 / 권한·소유자·크기 등 상세 정보. -a와 -l을 함께 쓸 수도 있다.
~ / . / ..사용자의 홈 디렉터리 / 현재 디렉터리 / 상위 디렉터리.
절대 경로 / 상대 경로/로 시작하면 루트부터 지정한다. robot_basic/hello_ros.py는 현재 폴더부터 찾는다.
mkdir -p필요한 상위 폴더도 만들며 이미 있는 폴더는 그대로 사용한다.
touch없는 파일은 빈 파일로 만들고, 있는 파일은 수정 시간을 갱신한다.

같은 이름의 바깥 ROS package 폴더와 안쪽 Python package 폴더는 역할이 다르다. ~/ros2_ws/src/turtle_robot은 package.xml과 setup.py가 있는 ROS package 루트이고, 그 안의 turtle_robot은 Python 모듈을 담는 폴더다. Python package에 넣는 __init__.py는 이 폴더를 일반 Python package로 인식하게 한다. 이 실습의 ros2_shell_practice는 파일 실행을 연습하는 폴더이며, ROS package는 14장의 ros2 pkg create로 생성한다. ament_python 패키지에서 setup.py의 console_scripts는 ros2 run의 실행 이름을 Python 함수에 연결한다. 등록을 바꾸었다면 다시 빌드·source한다.

3.4 Python 명령 실행과 파일 직접 실행#

텍스트 편집기로 ~/ros2_shell_practice/robot_basic/hello_ros.py를 열고 아래 내용을 저장한다. 첫 줄의 shebang은 파일을 직접 실행할 때 사용할 인터프리터를 운영체제에 알려준다. /usr/bin/env python3는 PATH에서 python3를 찾아 실행한다.

#!/usr/bin/env python3

print("Hello ROS 2")
cd ~/ros2_shell_practice
python3 robot_basic/hello_ros.py
chmod +x robot_basic/hello_ros.py
./robot_basic/hello_ros.py

두 실행 모두 Hello ROS 2를 출력한다. python3 파일.py는 Python 인터프리터가 읽을 수 있는 파일을 받아 실행하므로 파일 자체의 실행 권한은 필요하지 않다. ./파일.py는 파일 자체를 실행하므로 실행 권한과 올바른 shebang이 필요하다. chmod +x는 실행 권한을 추가하고, ./는 현재 디렉터리를 기준으로 경로를 지정한다. chmod는 ROS package 등록이나 console_scripts 생성을 대신하지 않는다.

Permission denied라면 ls -l로 실행 권한을 확인한다. No such file or directory라면 pwd와 파일 경로를 확인한다. 파일이 존재하는데 /usr/bin/env 관련 오류가 난다면 첫 줄이 정확한지, Windows 줄바꿈 CRLF가 섞였는지 확인하고 편집기에서 LF로 저장한다. which python3는 PATH에서 선택된 인터프리터를 보여준다. ROS apt 패키지를 사용할 때는 어떤 Python 환경을 실행하는지 함께 확인한다.

3.5 bashrc와 alias로 반복 명령 줄이기#

alias는 긴 셸 명령에 짧은 이름을 붙이는 기능이다. ~/.bashrc에 아래 두 줄을 넣으면 작업공간 루트로 이동하는 cw와 소스 폴더로 이동하는 cs를 사용할 수 있다. 텍스트 편집기로 기존 파일에 추가하고 같은 줄을 중복해서 넣지 않는다.

alias cw='cd ~/ros2_ws'
alias cs='cd ~/ros2_ws/src'
source ~/.bashrc
type cw
cw
pwd

source는 설정을 현재 셸에 적용한다. type cw는 cw가 어떤 alias로 정의되어 있는지 보여준다. cw 실행 뒤 pwd가 홈 디렉터리 아래 ros2_ws를 가리키면 정상이다. 다른 셸·새 PC에서는 alias가 정의되어 있지 않을 수 있으므로 명령의 원래 형태인 cd ~/ros2_ws도 함께 이해한다.

ROS 기본 환경은 /opt/ros/jazzy/setup.bash로 활성화하고, 자신의 workspace는 빌드가 끝난 뒤 ~/ros2_ws/install/setup.bash로 활성화한다. alias로 폴더를 이동하는 일과 source로 검색 경로를 적용하는 일은 별개다. echo $ROS_DOMAIN_ID가 빈 줄이어도 환경변수가 설정되지 않았다는 뜻일 수 있으며, 그 자체만으로 설치 실패를 뜻하지 않는다.

4장. Using turtlesim, ros2, and rqt#

4.1 turtlesim이 무엇인가#

turtlesim은 ROS 2 핵심 개념을 시각적으로 배우기 위한 가벼운 2D demo package다. 실제 robot driver 없이도 Node, Topic, Service, Parameter, Action, remapping을 모두 실습할 수 있어 공식 beginner tutorial의 공통 실습대상으로 사용된다.

4.2 설치와 실행#

sudo apt install -y ros-jazzy-turtlesim
ros2 run turtlesim turtlesim_node

ros2 run <package> <executable> 형식이다. turtlesim package의 turtlesim_node executable을 실행하면 /turtlesim Node가 Graph에 나타나고 GUI 창이 열린다.

4.3 teleop 실행#

# 다른 terminal
source /opt/ros/jazzy/setup.bash
ros2 run turtlesim turtle_teleop_key

teleop은 keyboard 입력을 받아 turtle의 속도명령 Topic으로 변환한다. terminal focus가 teleop 창에 있어야 key 입력이 전달된다.

4.4 rqt와 rqt_graph#

rqt
rqt_graph

rqt는 Qt 기반 ROS(Robot Operating System, 로봇 운영체제) GUI(Graphical User Interface, 그래픽 사용자 인터페이스) plugin framework이고 rqt_graph는 Node·Topic 연결관계를 시각화한다. 이 시점부터 CLI(Command-Line Interface, 명령줄 인터페이스)와 Graph를 함께 보면서 명령이 실제 연결을 어떻게 바꾸는지 확인한다.

5장. Understanding nodes#

5.1 실행 중 Node 확인#

ros2 node list
ros2 node info /turtlesim

node list는 현재 Graph에서 발견된 Node 이름을 보여준다. node info는 해당 Node가 가진 subscribers, publishers, services, action 등을 나열해 외부 interface를 확인한다.

5.2 Node 이름 Remapping#

ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
ros2 node list

--ros-args 뒤는 ROS 전용 실행 인자다. --remap은 이름 매핑을 바꾸고 __node는 Node 이름 자체를 지정하는 특별한 key다. 기존 /turtlesim을 rename하는 것이 아니라 새 process의 Node가 my_turtle이라는 이름으로 생성된다.

5.3 Node와 process의 관계#

하나의 process에 여러 Node가 있을 수 있고 같은 Node class를 여러 process에서 서로 다른 이름으로 실행할 수도 있다. ROS(Robot Operating System, 로봇 운영체제) CLI(Command-Line Interface, 명령줄 인터페이스)가 다루는 것은 OS(Operating System, 운영체제) PID(Process Identifier, 프로세스 식별자)보다 ROS Graph의 Node identity다.

6장. Understanding topics#

6.1 Topic 목록·타입 확인#

ros2 topic list
ros2 topic list -t
ros2 topic info /turtle1/cmd_vel -v

-t는 Topic 이름과 message type을 같이 보여준다. -v는 publisher/subscriber endpoint와 QoS(Quality of Service, 서비스 품질)까지 상세하게 보여준다.

6.2 Topic data 보기#

echo는 Topic이 흘리는 메시지의 실제 필드를 보여 주고, info는 발행자·구독자 연결을 보여 준다. 앞에서 본 Node와 Topic의 차이를 이 두 명령으로 확인한다.

ros2 topic echo /turtle1/pose
ros2 interface show turtlesim/msg/Pose

topic echo는 수신 message를 터미널에 출력한다. interface show는 해당 type의 field 구조를 보여준다. 값을 해석하려면 먼저 message type과 field 의미를 확인해야 한다.

6.3 CLI(Command-Line Interface, 명령줄 인터페이스)에서 직접 Publish#

ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 1.0}, angular: {z: 0.5}}"

ros2 topic pub --rate 10 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5}, angular: {z: 0.2}}"

--once는 한 번, --rate 10은 초당 10회 publish한다. 뒤의 중괄호는 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식) 형식의 message field 값이다. Topic은 subscriber가 응답을 반환하는 구조가 아니다.

6.4 Topic frequency 측정#

ros2 topic hz /turtle1/pose
ros2 topic bw /turtle1/pose

hz는 실제 수신 frequency, bw는 data bandwidth를 측정한다. 이후 센서 Topic 성능진단에서도 동일하게 사용한다.

7장. Understanding services#

7.1 Service 목록과 타입#

ros2 service list
ros2 service list -t
ros2 service type /spawn
ros2 interface show turtlesim/srv/Spawn

Service는 client가 request를 보내면 server가 response를 반환하는 구조다. service type과 request/response field를 확인한 뒤 call해야 한다.

7.2 turtle 생성 Service 호출#

ros2 service call /spawn turtlesim/srv/Spawn "{x: 2.0, y: 2.0, theta: 0.0, name: 'turtle2'}"

service call은 이름, service type, YAML(YAML Ain't Markup Language, 데이터 직렬화 형식) request 순으로 쓴다. 호출이 완료되면 response가 반환된다. 지속 작업이 아닌 짧은 request/response에 적합하다.

7.3 Service와 Topic 선택 기준#

앞 절의 Topic은 응답을 보장하지 않는 지속적 전달이고, Service는 한 요청에 한 응답을 돌려준다. 시간이 오래 걸리며 중간 진행률이 필요한 제어는 Action을 사용한다.

계속 흐르는 센서·상태는 Topic, 한 번 요청하고 즉시 결과를 받는 작업은 Service가 자연스럽다. 로봇팔의 관절각·TCP(Tool Center Point, 공구 중심점) 위치·카메라 영상·힘/토크처럼 계속 갱신되는 값은 Topic에 맞고, 그리퍼 열기·오류 상태 초기화처럼 짧게 끝나는 요청은 Service에 맞다. Home 이동이나 Pick처럼 수초 이상 걸리며 진행 상태·완료 결과·중도 취소가 필요한 작업은 Action을 사용한다. 전원 투입처럼 짧은 요청으로 보이더라도 실제 장비에서는 안전 승인과 상태 전이가 필요할 수 있으므로 하드웨어 인터페이스의 계약을 먼저 확인한다.

8장. Understanding parameters#

8.1 Parameter 목록과 값 확인#

Parameter는 Topic처럼 계속 발행하는 데이터가 아니라 특정 Node의 설정값이다. 변경 대상 Node와 Parameter 이름을 함께 지정한다.

ros2 param list /turtlesim
ros2 param get /turtlesim background_r

Parameter는 Node 안에 저장된 설정값이다. get은 현재 값을 읽고 set은 실행 중 값을 변경한다.

8.2 값 변경과 화면 갱신#

ros2 param set /turtlesim background_r 80
ros2 service call /clear std_srvs/srv/Empty "{}"

background_r를 바꾸고 clear service를 호출하면 배경색이 변경된다. Parameter 변경과 실제 동작 반영이 항상 자동으로 연결되는 것은 아니며 Node 구현에 따라 달라진다.

8.3 Parameter dump/load#

ros2 param dump /turtlesim > turtlesim.yaml
ros2 param load /turtlesim turtlesim.yaml

>는 명령 출력을 파일로 저장하는 shell redirection이다. dump 결과는 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)로 저장할 수 있고 load로 다시 적용할 수 있다. 재현 가능한 설정관리의 기본이다.

9장. Understanding actions#

9.1 Action을 직접 경험하기#

turtlesim teleop에서 방향회전 key를 누르면 회전 goal이 Action으로 실행되고 진행중 feedback과 완료 result를 받을 수 있다. 긴 작업이면서 중간 상태와 취소가 필요한 경우라는 Action의 특성을 시각적으로 확인할 수 있다.

9.2 Action 목록과 정보#

Action의 Goal은 시작 요청, Feedback은 진행 중 정보, Result는 완료 결과다. Service와 달리 작업 중 취소 요청을 받을 수 있다.

ros2 action list
ros2 action list -t
ros2 action info /turtle1/rotate_absolute
ros2 interface show turtlesim/action/RotateAbsolute

Action 정의는 Goal, Result, Feedback 세 message 구조를 포함한다. action info는 client/server 수를 확인한다.

9.3 CLI(Command-Line Interface, 명령줄 인터페이스)로 goal 보내기#

ros2 action send_goal /turtle1/rotate_absolute   turtlesim/action/RotateAbsolute "{theta: 1.57}" --feedback

--feedback을 붙이면 goal 실행 중 feedback을 계속 표시한다. Action은 Service와 달리 cancel과 진행상태가 핵심이다.

10장. rqt_console로 로그 보기#

10.1 로그와 severity#

ROS(Robot Operating System, 로봇 운영체제) logger는 DEBUG, INFO, WARN, ERROR, FATAL 같은 severity를 사용한다. rqt_console은 여러 Node가 내는 로그를 한 화면에서 필터링해 볼 수 있어 복잡한 시스템에서 어떤 Node가 문제를 보고했는지 찾기 쉽다.

rqt_console
ros2 run turtlesim turtlesim_node --ros-args --log-level WARN

--log-level WARN은 해당 실행의 logger threshold를 WARN 이상으로 올린다. 로그가 너무 많을 때 severity를 조절해 필요한 신호를 남긴다.

11장. Launching multiple nodes#

11.1 Launch가 필요한 이유#

실습 초기에는 Node마다 terminal을 열었지만 실제 robot은 수십 개 Node를 함께 실행한다. Launch는 어떤 program을 어떤 이름·namespace·parameter·remapping으로 실행할지 하나의 재현 가능한 구성으로 묶는다. Launch는 개별 노드의 내부 코드를 바꾸지 않고도 여러 노드의 실행 순서·이름·설정값을 함께 재현하는 실행 구성이다.

11.2 기존 Launch 실행#

ros2 launch turtlesim multisim.launch.py

ros2 launch <package> <launch_file> 형식이다. package에 설치된 launch file을 찾아 그 안에 정의된 여러 action을 실행한다.

11.3 Launch와 run 차이#

ros2 run은 executable 하나를 직접 실행하는 데 적합하고 ros2 launch는 여러 Node와 설정을 함께 구성하는 데 적합하다. 뒤의 client library 파트에서 직접 launch file을 작성한다.

12장. rosbag2로 데이터 기록·재생#

12.1 Topic 기록#

mkdir -p ~/bag_files
cd ~/bag_files
ros2 bag record /turtle1/cmd_vel /turtle1/pose

ros2 bag record는 선택한 Topic message와 timestamp를 storage에 저장한다. 실제 장비에서 한 번 발생한 상황을 나중에 반복 재현하는 데 매우 중요하다.

12.2 bag 정보 확인#

ros2 bag info <bag_directory>

bag info는 duration, message count, Topic type과 개수 등 기록 metadata를 보여준다.

12.3 재생#

ros2 bag play <bag_directory>

bag play는 기록한 message를 다시 publish한다. turtlesim을 새로 실행한 상태에서 재생하면 기록한 cmd_vel 흐름이 다시 전달되는 것을 볼 수 있다. 뒤의 SLAM·vision 디버깅에서도 동일한 방식으로 sensor 입력을 재현한다.

Part III. Tutorials — Beginner: Client libraries#

13장. colcon으로 ROS 2 package 빌드하기#

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

13.1 colcon의 역할#

colcon은 Python 파일을 직접 실행하는 명령이 아니라 여러 ROS(Robot Operating System, 로봇 운영체제) package의 빌드와 설치를 지휘하는 도구다. 빌드한 뒤 source해야 현재 터미널이 새 package를 찾을 수 있다.

colcon은 workspace 안 여러 package의 의존관계를 보고 각 package의 실제 빌드도구를 올바른 순서로 호출하는 meta-build/orchestration 도구다. CMake나 setuptools를 대체하는 compiler가 아니다.

13.2 workspace 만들기와 source 추가#

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
# 예제 package나 자신의 package를 이곳에 둔다
cd ~/ros2_ws

workspace는 여러 ROS package를 함께 빌드·실행하기 위한 작업공간이다. 관례적으로 원본 package는 src 아래에 둔다.

13.3 Underlay source#

source /opt/ros/jazzy/setup.bash

/opt/ros/jazzy는 이미 설치된 base environment(underlay)다. workspace를 빌드하면 그 위에 자신의 install 공간을 overlay로 source한다.

13.4 build·test·source#

cd ~/ros2_ws
colcon build --symlink-install
colcon test
colcon test-result --verbose
source install/setup.bash

build 후 build/install/log가 생긴다. install/setup.bash를 source하면 현재 shell에서 새 package를 ros2 run/launch로 찾을 수 있다.

13.5 선택 빌드#

colcon list
colcon build --packages-select my_package
colcon build --packages-up-to my_app

--packages-select는 지정 package만, --packages-up-to는 지정 package와 dependency까지 빌드한다.

14장. Workspace와 Package 만들기#

14.1 workspace 구조#

ros2_ws/
├── src/
├── build/
├── install/
└── log/

src는 원본, build/install/log는 colcon이 만드는 산출물이다. Git에는 일반적으로 src의 source와 configuration을 넣고 build/install/log는 제외한다.

14.2 Python package 생성#

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0   --dependencies rclpy std_msgs py_pubsub

--build-type ament_python은 Python package용 ament 규칙을 사용한다는 뜻이다. --dependencies는 package.xml에 dependency를 추가하고 --license는 package metadata를 설정한다.

14.3 생성 파일 읽기#

파일 역할
package.xml package metadata와 dependency
setup.py Python 설치 규칙·console_scripts entry point
setup.cfg script 설치 위치
resource/<pkg> ament index에 package 등록
<pkg>/__init__.py Python package 표시

14.4 NumPy로 로봇 데이터 다루기#

NumPy는 센서값·관절값·이미지·좌표 행렬처럼 여러 수를 한꺼번에 계산하는 Python 라이브러리다. 핵심 객체 ndarray는 같은 자료형의 데이터를 여러 차원의 배열로 저장한다. Python list의 +는 목록을 이어 붙이지만 NumPy 배열의 +는 대응하는 요소를 더한다. 로봇의 여러 관절에 같은 보정값을 더하거나 센서값의 평균을 구할 때 반복문 대신 배열 연산을 사용할 수 있다.

sudo apt install python3-numpy
python3 -c "import numpy as np; print(np.__version__)"

apt의 python3-numpy는 Ubuntu의 Python 환경에 NumPy를 설치한다. python3 -c는 뒤의 문자열을 짧은 Python 프로그램으로 실행하며, np.__version__은 설치된 NumPy 버전이다. 아래 예제를 numpy_practice.py에 저장하고 python3 numpy_practice.py로 실행한다.

import numpy as np

angles = [30.0, 45.0, -10.0]
joint = np.array(angles, dtype=np.float64)
offset = np.array([1.0, -2.0, 3.0])

print(angles + angles)
print(joint + offset)
print(joint * 2)
print(np.mean(joint))
print(joint.shape, joint.ndim, joint.dtype)
print(joint.tolist())

np.array는 Python 목록에서 ndarray를 만든다. dtype=np.float64는 소수 계산을 위한 64비트 실수 자료형을 지정한다. joint + offset은 [31. 43. -7.], joint * 2는 [60. 90. -20.]을 출력한다. np.mean은 전체 요소의 평균을 구한다. shape는 각 축의 크기이며 이 예제에서는 (3,), ndim은 축의 개수여서 1이다. tolist()는 배열을 Python 목록으로 돌려준다. ROS(Robot Operating System, 로봇 운영체제) 메시지에 값을 넣을 때 메시지 필드가 요구하는 자료형과 단위도 함께 맞춘다.

import numpy as np

data = np.array([[1, 2, 3],
                 [4, 5, 6],
                 [7, 8, 9]])
print(data.shape)
print(data[0, :])
print(data[:, -1])
print(data[:, 1])
print(data.reshape(-1))
print(data.sum(axis=1))
print(data.sum(axis=0))
연산출력해석
data.shape(3, 3)행 3개, 열 3개인 2차원 배열.
data[0, :][1 2 3]첫 번째 행의 모든 열.
data[:, -1][3 6 9]모든 행의 마지막 열.
data[:, 1][2 5 8]모든 행에서 인덱스 1인 열.
data.reshape(-1)[1 2 3 4 5 6 7 8 9]기본 행 우선 순서로 1차원 배열 형태로 바꾼다.
data.sum(axis=1)[6 15 24]열 방향을 합쳐 행별 합을 만든다.
data.sum(axis=0)[12 15 18]행 방향을 합쳐 열별 합을 만든다.

NumPy의 다차원 인덱싱은 [행, 열]처럼 축마다 인덱스를 지정한다. :는 해당 축 전체를 선택한다. axis는 연산으로 줄일 축의 번호다. shape=(행, 열)에서는 axis=0이 행 축, axis=1이 열 축이다. reshape는 요소의 총수를 유지하면서 배열 형태를 바꾸며 -1은 해당 크기를 NumPy가 계산하도록 한다.

기본 슬라이싱은 원본 메모리를 공유하는 view를 반환할 수 있다. 잘라 낸 배열을 바꿀 때 원본도 바뀌지 않게 하려면 data[:, 1].copy()처럼 명시적으로 복사한다. 이미지가 shape=(높이, 너비, 채널)로 표현되면 image[y, x, channel] 순서로 접근한다. 채널의 색상 순서는 사용하는 라이브러리와 영상 형식에서 확인한다.

import numpy as np

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(a * b)
print(a @ b)

*는 같은 위치의 요소끼리 곱하므로 [[5, 12], [21, 32]]가 된다. @는 행렬곱이므로 [[19, 22], [43, 50]]가 된다. 두 행렬의 행렬곱은 앞 행렬의 열 수와 뒤 행렬의 행 수가 같아야 한다. 좌표 변환에서 회전 행렬을 점 벡터에 적용할 때는 이런 선형대수 연산의 의미를 확인해 사용한다.

15장. Publisher와 Subscriber — Python#

Python의 rclpy로 Publisher와 Subscriber를 구현한다. 먼저 타이머 기반 발행과 수신 콜백을 만들고, 같은 Topic 이름·메시지 타입·호환되는 QoS(Quality of Service, 서비스 품질)가 통신을 결정하는 원리를 turtlesim에서 확인한다.

rclcpp::Node를 상속하고 create_publisher<MessageT>()와 create_wall_timer()를 사용한다. SharedPtr는 publisher와 timer 객체의 수명을 Node와 함께 관리한다. Python의 create_publisher()/create_timer()와 개념은 같다.

15.1 Python Publisher#

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class MinimalPublisher(Node):
    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        self.timer = self.create_timer(0.5, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = f'Hello World: {self.i}'
        self.publisher_.publish(msg)
        self.i += 1

Python Publisher는 Node를 상속해 만들고 create_publisher()로 발행 대상을 등록한다. create_timer()의 Callback은 지정한 주기마다 Executor가 실행하는 함수다. Topic의 이름과 Message 타입이 수신 노드와 맞아야 메시지가 전달된다.

15.1.1 turtlesim 속도 명령 Publisher#

/turtle1/cmd_vel은 geometry_msgs/msg/Twist 메시지로 거북이의 선속도와 각속도를 받는다. 평면 이동에서는 linear.x가 전진 속도, angular.z가 회전 속도다. turtlesim의 위치·선속도 단위는 시뮬레이터 좌표 단위이고, angular.z는 rad/s이므로 실제 이동로봇의 m/s와 동일하다고 가정하지 않는다.

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class TurtleVelocityPublisher(Node):
    def __init__(self):
        super().__init__('lesson_05_cmd_vel_publisher')
        self.publisher = self.create_publisher(
            Twist, '/turtle1/cmd_vel', 10)
        self.timer = self.create_timer(0.1, self.publish_velocity)

    def publish_velocity(self):
        message = Twist()
        message.linear.x = 1.0
        message.angular.z = 0.0
        self.publisher.publish(message)


def main(args=None):
    rclpy.init(args=args)
    node = TurtleVelocityPublisher()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

create_publisher(Twist, '/turtle1/cmd_vel', 10)의 마지막 10은 기본 QoS의 queue depth다. create_timer(0.1, ...)은 노드 clock을 기준으로 callback을 약 0.1초마다 executor에 준비시키므로 목표 발행률은 10 Hz다. 운영체제 스케줄링과 callback 부하로 실제 주기는 조금 흔들릴 수 있으며, timer callback 안에서 sleep하면 다음 callback과 다른 처리를 막을 수 있다. 여기서 QoS depth=10은 아직 처리하지 못한 메시지를 얼마나 보관할지 결정하는 큐 크기다. QoS는 단순 속도 설정이 아니라 발행자와 구독자가 합의해야 하는 전달 조건이다.

setup.py의 console_scripts에 실행 이름을 등록한 뒤 선택 빌드하고 workspace를 source한다. --packages-select는 지정한 패키지만 빌드하고, --symlink-install은 Python 소스 수정이 install 공간에 심볼릭 링크로 반영되게 해 반복 개발을 빠르게 한다.

'lesson_05_cmd_vel_publisher = turtle_robot.lesson_05_cmd_vel_publisher:main',

cd ~/ros2_ws
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash

turtlesim과 Publisher를 서로 다른 터미널에서 실행하고, 세 번째 터미널에서 실제 메시지 값과 발행률을 확인한다. 다른 teleop이나 제어 노드가 같은 토픽을 동시에 발행하면 명령이 번갈아 도착해 움직임이 불안정해질 수 있으므로 먼저 종료한다.

# 터미널 A
ros2 run turtlesim turtlesim_node

# 터미널 B
ros2 run turtle_robot lesson_05_cmd_vel_publisher

# 터미널 C
ros2 topic echo /turtle1/cmd_vel
ros2 topic hz /turtle1/cmd_vel

echo에서 linear.x가 1.0, angular.z가 0.0으로 보이고 topic hz가 약 10 Hz이면 의도한 메시지와 주기가 전달되는 것이다. Ctrl+C는 현재 터미널의 노드를 종료하지만 다른 터미널의 노드까지 종료하지 않으므로 실습을 마칠 때 각각 정리한다.

15.2 Python Subscriber#

class MinimalSubscriber(Node):
    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String, 'topic', self.listener_callback, 10)

    def listener_callback(self, msg):
        self.get_logger().info(f'I heard: {msg.data}')

15.3 setup.py entry point#

entry_points={
    'console_scripts': [
        'talker = py_pubsub.publisher_member_function:main',
        'listener = py_pubsub.subscriber_member_function:main',
    ],
}

Python은 setup.py의 console_scripts가 ros2 run에서 사용할 executable 이름과 Python main 함수를 연결한다. 새 실행 이름을 추가하거나 변경했을 때는 colcon build 뒤 source install/setup.bash를 다시 실행한다. Python 파일 경로와 ros2 run의 실행 이름은 console_scripts가 연결한다.

15.4 Build·Run과 통신 검증#

cd ~/ros2_ws
colcon build --packages-select py_pubsub --symlink-install
source install/setup.bash

# 서로 다른 터미널에서 실행
ros2 run py_pubsub talker
ros2 run py_pubsub listener

# 별도 터미널에서 Graph 확인
ros2 topic info /topic
ros2 topic echo /topic

Python으로 만든 Publisher와 Subscriber는 같은 Topic 이름, 일치하는 Message 타입, 호환되는 QoS를 통해 연결된다. source install/setup.bash는 빌드한 패키지를 현재 셸의 검색 경로에 추가하며, ros2 topic info는 이 연결에 참여한 Publisher·Subscription 수를 확인하는 명령이다.

15.5 turtlesim 위치 메시지 구독하기#

문자열 토픽에서 출발해 실제 상태 메시지로 구독 대상을 바꿔 보자. turtlesim의 /turtle1/pose는 turtlesim/msg/Pose 타입을 사용한다. x와 y는 시뮬레이터 좌표계의 위치이고 theta는 라디안 단위의 방향각이다. 화면 픽셀 좌표와 물리 로봇의 미터 좌표를 그대로 혼동하지 않는다. 메시지 타입은 데이터 구조이고 토픽 이름은 그 데이터를 보내는 채널 이름이다.

새 패키지 turtle_robot을 만들고, 안쪽 Python package 폴더에 노드 파일을 저장한다. 아래 package 생성 명령은 새 패키지를 만들 때 한 번 실행한다. 이미 같은 패키지를 만들었다면 기존 구조와 의존성을 확인해 사용한다.

source /opt/ros/jazzy/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create turtle_robot --build-type ament_python --dependencies rclpy turtlesim

--build-type ament_python은 Python 패키지의 빌드 체계를 선택한다. --dependencies는 생성할 package.xml에 의존 패키지를 등록한다. rclpy는 Python ROS 2(Robot Operating System 2, 로봇 운영체제 2) API(Application Programming Interface, 응용 프로그램 인터페이스)이며 turtlesim은 Pose·Color 메시지를 제공한다. package.xml에 두 패키지의 depend 항목이 있는지 확인한다.

터미널 A에서 ros2 run turtlesim turtlesim_node를 실행하고 터미널 B에서 ros2 run turtlesim turtle_teleop_key를 실행한다. 각 터미널에서 먼저 Jazzy 환경을 source한다. 방향키 입력은 turtle_teleop_key가 실행 중인 터미널에 포커스를 두고 보낸다. 터미널 C에서는 다음으로 메시지 구조를 확인한다.

ros2 topic type /turtle1/pose
ros2 interface show turtlesim/msg/Pose
ros2 topic echo /turtle1/pose

type은 토픽의 타입을 확인하고 interface show는 그 타입의 필드 정의를 보여준다. echo는 수신 메시지를 계속 출력하며 Ctrl+C로 종료한다. Pose에는 x·y·theta 외에 linear_velocity·angular_velocity도 있다. 이번 노드는 위치와 방향만 출력한다.

파일 경로: ~/ros2_ws/src/turtle_robot/turtle_robot/lesson_01_pose_subscriber.py

import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from turtlesim.msg import Pose


class PoseSubscriber(Node):
    def __init__(self):
        super().__init__('lesson_01_pose_subscriber')
        self.subscription = self.create_subscription(
            Pose, '/turtle1/pose', self.on_pose, 10)

    def on_pose(self, msg):
        self.get_logger().info(
            f'x={msg.x:.2f}, y={msg.y:.2f}, '
            f'theta={msg.theta:.2f} rad')


def main(args=None):
    rclpy.init(args=args)
    node = PoseSubscriber()
    try:
        rclpy.spin(node)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

create_subscription의 인자는 메시지 타입, 토픽 이름, 수신 callback, QoS 설정 순이다. 여기서 정수 10은 KEEP_LAST 방식의 depth를 지정한다. QoS는 대기 메시지 보관 방식과 전달 정책을 정하며, depth는 수신이 밀릴 때 보관할 최신 메시지 수다. self.on_pose는 함수를 지금 호출하지 않고 callback으로 등록한다. 실제 메시지를 받으면 executor가 msg를 인자로 전달해 호출한다.

get_logger().info는 이 노드 이름과 시각을 붙여 INFO 수준 로그를 남긴다. rclpy.init은 ROS(Robot Operating System, 로봇 운영체제) 통신 context를 초기화하고 spin은 준비된 callback을 처리한다. destroy_node는 노드의 자원을 정리하고 shutdown은 context를 종료한다. rclpy.ok는 context가 아직 활성화되어 있는지 확인한다. Ctrl+C로 중단하더라도 정리 경로가 실행되도록 main을 구성했다. ExternalShutdownException은 context가 외부에서 종료되어 spin을 계속할 수 없을 때 발생한다. 이 예제에서는 키보드 중단과 함께 종료 경로로 처리한다.

setup.py의 기존 console_scripts 목록에 다음 항목을 추가한다. 기존 entry_points를 통째로 새로 만들지 않고 목록에 실행 이름을 등록한다.

'lesson_01_pose_subscriber = turtle_robot.lesson_01_pose_subscriber:main',

등호 왼쪽이 ros2 run에서 입력할 실행 이름이다. 오른쪽은 Python package.모듈:함수다. 파일명이 같더라도 등록한 실행 이름이 다르면 ros2 run에는 왼쪽 이름을 입력해야 한다. setup.cfg의 script_dir와 install_scripts가 $base/lib/turtle_robot을 가리키는지도 확인한다. 이 경로가 ROS에서 Python 실행 명령을 찾는 설치 위치다.

cd ~/ros2_ws
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash
ros2 pkg executables turtle_robot
ros2 run turtle_robot lesson_01_pose_subscriber

--packages-select는 지정한 패키지를 선택해서 빌드하고 --symlink-install은 지원되는 파일을 링크해 소스 수정이 설치 공간에 반영되게 한다. setup.py의 실행 등록이나 의존성을 바꿨다면 다시 빌드한다. source install/setup.bash는 빌드 결과를 현재 터미널의 검색 경로에 반영한다. ros2 pkg executables는 등록된 실행 이름을 확인한다. 거북이를 움직이면 x·y·theta가 바뀌어 출력되는지 확인한다.

15.6 바닥 색상 메시지 구독하기#

/turtle1/color_sensor는 거북이 위치에서 읽은 바닥 색상을 turtlesim/msg/Color로 보낸다. r·g·b는 각각 0~255 범위의 빨강·초록·파랑 값이다. Pose와 같은 subscriber 구조를 쓰지만 토픽 이름과 메시지 타입, 읽는 필드가 바뀐다.

ros2 topic type /turtle1/color_sensor
ros2 interface show turtlesim/msg/Color
ros2 topic echo /turtle1/color_sensor

파일 경로: ~/ros2_ws/src/turtle_robot/turtle_robot/lesson_02_color_subscriber.py

import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from turtlesim.msg import Color


class ColorSubscriber(Node):
    def __init__(self):
        super().__init__('lesson_02_color_subscriber')
        self.subscription = self.create_subscription(
            Color, '/turtle1/color_sensor', self.on_color, 10)

    def on_color(self, msg):
        self.get_logger().info(
            f'R={msg.r}, G={msg.g}, B={msg.b}')


def main(args=None):
    rclpy.init(args=args)
    node = ColorSubscriber()
    try:
        rclpy.spin(node)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

setup.py의 console_scripts에 아래 항목을 추가하고 15.8과 같은 빌드·source 절차를 수행한다. run의 실행 이름만 lesson_02_color_subscriber로 바꾼다.

'lesson_02_color_subscriber = turtle_robot.lesson_02_color_subscriber:main',

위치가 달라져도 균일한 바닥에서는 RGB(Red, Green, Blue, 적·녹·청 색상) 값이 그대로일 수 있다. 숫자가 변하지 않는 것과 메시지가 도착하지 않는 것을 구분한다. echo에서 메시지가 반복된다면 전송은 이루어지고 있다. background_r·background_g·background_b parameter를 바꾸고 /clear를 호출하면 배경색을 바꿔 관찰할 수 있다.

ros2 param set /turtlesim background_r 200
ros2 param set /turtlesim background_g 40
ros2 param set /turtlesim background_b 80
ros2 service call /clear std_srvs/srv/Empty "{}"

param set은 실행 중인 노드의 설정값을 바꾼다. /clear는 그림과 배경을 새 설정으로 갱신하는 service이며 Empty는 요청 데이터가 없는 service 타입이다. {}는 비어 있는 요청을 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식) 형식으로 표현한다. 거북이가 지나간 선의 색과 배경의 색은 다를 수 있으므로 선이 없는 곳에서 관찰해 본다.

15.7 한 노드에서 위치와 색상을 함께 출력하기#

두 subscription을 한 노드에 등록하고 각 callback에서 가장 최근 메시지를 저장한다. 별도의 timer callback은 1초마다 저장된 값을 읽어 한 줄로 출력한다. 메시지 수신 주기와 출력 주기를 분리하면 빠른 토픽을 모두 로그에 쓰지 않고도 현재 상태를 관찰할 수 있다.

파일 경로: ~/ros2_ws/src/turtle_robot/turtle_robot/lesson_03_pose_color_subscriber.py

import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from turtlesim.msg import Color, Pose


class PoseColorSubscriber(Node):
    def __init__(self):
        super().__init__('lesson_03_pose_color_subscriber')
        self.latest_pose = None
        self.latest_color = None
        self.pose_subscription = self.create_subscription(
            Pose, '/turtle1/pose', self.on_pose, 10)
        self.color_subscription = self.create_subscription(
            Color, '/turtle1/color_sensor', self.on_color, 10)
        self.timer = self.create_timer(1.0, self.report)

    def on_pose(self, msg):
        self.latest_pose = msg

    def on_color(self, msg):
        self.latest_color = msg

    def report(self):
        if self.latest_pose is None or self.latest_color is None:
            return
        pose = self.latest_pose
        color = self.latest_color
        self.get_logger().info(
            f'위치 x={pose.x:.2f}, y={pose.y:.2f} | '
            f'색상 R={color.r}, G={color.g}, B={color.b}')


def main(args=None):
    rclpy.init(args=args)
    node = PoseColorSubscriber()
    try:
        rclpy.spin(node)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

create_timer(1.0, self.report)는 노드의 clock을 기준으로 1초 간격 timer를 만들고 report를 callback으로 등록한다. timer도 spin 중 executor가 실행한다. 최신 값은 첫 메시지 이전에 None으로 시작하므로 두 토픽을 모두 받은 뒤에만 출력한다. subscription callback에서는 값을 저장하고 바로 돌아가며, sleep으로 1초를 기다리지 않는다. 하나의 spin에서 두 수신 callback과 timer가 처리된다.

이 코드는 각 토픽의 최신 값을 묶는 방식이다. 두 메시지가 정확히 같은 시각에 측정됐다는 뜻은 아니다. Pose와 Color에는 Header timestamp가 없으므로 이 실습에서 측정 시각 기준 동기화를 할 수 없다. 실제 카메라·IMU(Inertial Measurement Unit, 관성측정장치) 등을 결합할 때는 Header의 timestamp와 message_filters 같은 동기화 방법을 검토한다. 토픽 하나가 멈추면 저장된 마지막 값이 계속 남으므로 실장비에서는 수신 시각과 데이터 유효시간도 관리한다.

'lesson_03_pose_color_subscriber = turtle_robot.lesson_03_pose_color_subscriber:main',

setup.py에 실행 이름을 등록한 뒤 다시 빌드하고 새 터미널에서 Jazzy와 overlay를 source한다.

source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
ros2 run turtle_robot lesson_03_pose_color_subscriber

turtlesim이 실행 중이면 대략 1초마다 위치와 RGB 값이 한 줄로 출력된다. 위치 숫자는 현재 거북이 상태에 따라 달라진다. turtlesim을 실행하지 않으면 두 메시지가 도착하지 않으므로 출력하지 않는다.

15.8 관찰과 오류 진단 실습#

ros2 node list
ros2 node info /lesson_03_pose_color_subscriber
ros2 topic info /turtle1/pose --verbose
ros2 topic hz /turtle1/pose
rqt_graph

node info에서 두 subscription을 확인하고 rqt_graph에서 turtlesim이 두 토픽을 통해 같은 subscriber로 연결되는지 본다. topic info --verbose는 endpoint별 상세 정보와 QoS를 보여준다. hz는 해당 관찰 subscriber가 받은 메시지의 빈도를 측정한다. 이 값은 timer의 로그 출력 빈도와 다르며 시스템 부하와 QoS의 영향을 받을 수 있다.

증상먼저 확인할 항목
Package not found같은 터미널에서 install/setup.bash를 source했는지, 패키지 빌드가 완료됐는지 확인한다.
No executable foundconsole_scripts 왼쪽 실행 이름, setup.cfg 설치 경로와 재빌드를 확인한다.
ModuleNotFoundErrorPython package·파일명·의존 패키지와 which python3를 확인한다.
노드는 보이는데 출력이 없다turtlesim 실행 여부, 토픽 이름·타입, 두 토픽의 수신 여부와 QoS를 확인한다.
RGB 숫자가 계속 같다균일한 바닥에서는 정상일 수 있다. echo로 수신 여부를 따로 확인한다.

직접 실험해 보자. 먼저 timer 주기를 0.5초로 바꿔 출력 주기만 달라지는지 확인한다. 다음으로 한 subscription의 토픽 이름을 잘못 지정해 두 메시지를 기다리는 조건을 관찰한다. 마지막으로 get_logger 대신 출력 내용을 바꾸고, 코드 파일 수정과 console_scripts 수정에서 각각 필요한 빌드 절차를 구분해 설명한다.

15.9 위치를 읽고 벽 앞에서 회전하기#

15.4.1의 속도 Publisher에 Pose Subscriber를 결합한다. /turtle1/pose에서 받은 최신 위치와 방향을 저장하고, 0.1초마다 앞으로 갈지 제자리에서 회전할지 결정한다. 첫 위치 메시지가 오기 전에는 제어 명령을 발행하지 않는다. 파일은 ~/ros2_ws/src/turtle_robot/turtle_robot/lesson_06_turtle_cmd_and_pose.py에 저장한다.

import math
import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from geometry_msgs.msg import Twist
from turtlesim.msg import Pose


class TurtleCmdAndPose(Node):
    def __init__(self):
        super().__init__('lesson_06_turtle_cmd_and_pose')
        self.pose = None
        self.publisher = self.create_publisher(
            Twist, '/turtle1/cmd_vel', 10)
        self.subscription = self.create_subscription(
            Pose, '/turtle1/pose', self.receive_pose, 10)
        self.timer = self.create_timer(0.1, self.control)

    def receive_pose(self, message):
        self.pose = message

    def control(self):
        if self.pose is None:
            return
        p = self.pose
        dx = math.cos(p.theta)
        dy = math.sin(p.theta)
        toward_wall = (
            (p.x < 1.5 and dx < 0)
            or (p.x > 9.5 and dx > 0)
            or (p.y < 1.5 and dy < 0)
            or (p.y > 9.5 and dy > 0)
        )
        message = Twist()
        if toward_wall:
            message.angular.z = 1.5
        else:
            message.linear.x = 1.0
        self.publisher.publish(message)


def main(args=None):
    rclpy.init(args=args)
    node = TurtleCmdAndPose()
    try:
        rclpy.spin(node)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    finally:
        if rclpy.ok():
            node.publisher.publish(Twist())
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()

theta는 rad 단위 방향각이다. cos(theta)와 sin(theta)는 각각 화면의 x축·y축 방향으로 향하는 성분이며 이동 거리 자체가 아니다. 왼쪽 경계 근처이면서 dx < 0이면 왼쪽 벽을 향하고, 오른쪽 경계 근처이면서 dx > 0이면 오른쪽 벽을 향한다. 아래·위 경계는 dy의 부호로 판정한다. 따라서 벽 가까이 있어도 화면 안쪽을 향한다면 전진한다.

1.5와 9.5는 이 실습에서 미리 회전하기 위한 turtlesim 좌표 임계값이다. 조건이 참이면 linear.x는 0이고 angular.z만 1.5이므로 제자리에서 반시계 방향으로 회전한다. 조건이 거짓이면 linear.x만 1.0이다. 매 주기 새 Twist를 만들면 이전 전진·회전 값이 다음 명령에 남지 않는다. 실제 로봇에서는 센서, 제동거리, 로봇 크기와 좌표계에 맞춰 제어 기준을 다시 설계해야 한다.

setup.py의 console_scripts 목록에 아래 entry point를 추가한다. package.xml에는 rclpy, geometry_msgs, turtlesim의 실행 의존성이 필요하다. 기존 turtle_robot 패키지에서 해당 의존성을 먼저 확인한다.

'lesson_06_turtle_cmd_and_pose = turtle_robot.lesson_06_turtle_cmd_and_pose:main',
cd ~/ros2_ws
source /opt/ros/jazzy/setup.bash
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash
ros2 run turtle_robot lesson_06_turtle_cmd_and_pose

다른 터미널에서 Jazzy를 source하고 turtlesim_node를 실행한다. teleop과 lesson_05 속도 Publisher는 종료해 같은 cmd_vel 토픽에 다른 명령이 섞이지 않게 한다. 아래 teleport_absolute 서비스로 거북이를 오른쪽 벽 가까이, 바깥쪽 방향(theta=0)으로 놓으면 회전하다가 화면 안쪽을 향할 때 전진하는 것을 볼 수 있다. 서비스 이름과 타입은 ros2 service list -t로 확인할 수 있다.

ros2 run turtlesim turtlesim_node

# 검증용 별도 터미널
ros2 service call /turtle1/teleport_absolute \
  turtlesim/srv/TeleportAbsolute "{x: 10.0, y: 5.5, theta: 0.0}"
ros2 topic echo /turtle1/cmd_vel
# echo를 Ctrl+C로 종료한 뒤
ros2 topic hz /turtle1/cmd_vel

회전 중에는 linear.x=0.0, angular.z=1.5이고 전진 중에는 linear.x=1.0, angular.z=0.0인지 확인한다. 발행률은 약 10Hz가 목표다. 오른쪽 경계에서 theta를 약 3.14로 두면 안쪽을 향하므로 바로 전진해야 한다. 이 두 경우를 비교하면 위치만으로 회전하는 구현과 방향까지 판정하는 구현의 차이를 확인할 수 있다. 종료 시 0 속도를 한 번 발행하지만 DDS(Data Distribution Service, 데이터 분산 서비스) 전달을 보장하는 절차는 아니며, turtlesim의 명령 timeout 동작과 구분해서 이해한다.

16장. Service와 Client — Python#

Python의 rclpy로 Service Server와 Client를 구현한다. Service는 요청(Request)을 보내고 응답(Response)을 돌려받는 짧은 작업에 쓰며, 실행 중인 노드가 다른 일을 계속해야 할 때는 call_async()와 Future를 이용한다.

Service server는 service type, 이름, callback을 등록한다. callback은 typed Request를 읽고 Response를 채운다.

async_send_request()는 즉시 future를 반환한다. executor가 response event를 처리한 뒤 future가 완료된다.

16.1 Python Service#

from example_interfaces.srv import AddTwoInts

self.srv = self.create_service(
    AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

def add_two_ints_callback(self, request, response):
    response.sum = request.a + request.b
    return response

16.2 Python Client#

self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
    self.get_logger().info('service not available, waiting...')

request = AddTwoInts.Request()
request.a = 2
request.b = 3
future = self.cli.call_async(request)

wait_for_service()는 server discovery를 기다리고 call_async()는 thread를 막지 않은 채 Future를 반환한다. Future는 결과가 당장 준비되지 않았음을 표현하는 객체다. call_async() 직후에 결과를 읽는 것이 아니라 Executor가 응답을 처리한 뒤 future.result()로 꺼낸다.

16.2.1 turtlesim Service Client: 순간이동과 생성#

turtlesim의 /turtle1/teleport_absolute와 /spawn은 같은 Client 흐름을 연습하기 좋다. 먼저 ros2 service type과 ros2 interface show로 서비스 타입과 Request 필드를 확인한다. Topic 메시지와 달리 Service는 Request와 Response가 한 타입 안에 함께 정의된다.

ros2 service type /turtle1/teleport_absolute
ros2 interface show turtlesim/srv/TeleportAbsolute
ros2 service type /spawn
ros2 interface show turtlesim/srv/Spawn

아래 노드는 거북이를 (2.0, 2.0), theta=0.0 rad로 한 번 이동시킨 뒤 종료한다. create_client()의 첫 인자는 서비스 타입, 둘째 인자는 ROS(Robot Operating System, 로봇 운영체제) Graph에 등록된 서비스 이름이다. wait_for_service()는 서버가 discovery될 때까지 확인하고, call_async()는 Request를 보내면서 Future를 즉시 반환한다.

import rclpy
from rclpy.node import Node
from turtlesim.srv import TeleportAbsolute


class TeleportClient(Node):
    def __init__(self):
        super().__init__('lesson_08_teleport_client')
        self.client = self.create_client(
            TeleportAbsolute, '/turtle1/teleport_absolute')

    def send_request(self):
        request = TeleportAbsolute.Request()
        request.x = 2.0
        request.y = 2.0
        request.theta = 0.0
        return self.client.call_async(request)


def main(args=None):
    rclpy.init(args=args)
    node = TeleportClient()
    try:
        while not node.client.wait_for_service(timeout_sec=1.0):
            node.get_logger().info('teleport service를 기다리는 중')
        future = node.send_request()
        rclpy.spin_until_future_complete(node, future)
        future.result()
        node.get_logger().info('순간이동 완료')
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

spin_until_future_complete()는 executor가 Service 응답 이벤트를 처리하도록 spin하면서 지정한 Future의 완료를 기다린다. 한 번 요청하고 프로그램을 끝내는 명령형 Client에 적합하다. future.result()는 완료된 Response를 꺼내며, 통신 실패나 서버 측 오류가 있으면 예외가 발생할 수 있으므로 실제 애플리케이션에서는 try/except로 실패 경로도 기록한다. 여기서 Executor는 콜백을 실제로 실행하는 ROS 2(Robot Operating System 2, 로봇 운영체제 2) 실행기다. spin_until_future_complete()는 Future가 끝날 때까지 Executor가 이벤트를 처리하게 하므로, 다른 콜백 안에서 무심코 다시 호출하지 않는다.

/spawn으로 바꿀 때는 Client 타입과 Request 필드가 달라진다. Spawn Response의 name에는 실제로 생성된 거북이 이름이 들어온다. 같은 이름이 이미 존재하면 요청이 실패하므로 이름을 바꾸거나 turtlesim을 다시 시작한다.

from turtlesim.srv import Spawn

self.client = self.create_client(Spawn, '/spawn')
request = Spawn.Request()
request.x = 8.0
request.y = 8.0
request.theta = 0.0
request.name = 'turtle2'
future = self.client.call_async(request)

# Future가 완료된 뒤
response = future.result()
self.get_logger().info(f'생성된 이름: {response.name}')
cd ~/ros2_ws
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash
ros2 run turtlesim turtlesim_node
# 다른 터미널에서
ros2 run turtle_robot lesson_08_teleport_client

16.2.2 한 번 기다리기와 응답 콜백#

Service 응답을 처리하는 방식은 노드의 수명에 따라 고른다. 한 번 호출하고 종료하는 Client는 spin_until_future_complete()로 기다릴 수 있다. Subscriber·Timer와 함께 계속 실행되는 노드는 응답 콜백을 등록해 기존 callback 처리를 멈추지 않는 편이 안전하다.

# 한 번 호출하고 종료하는 프로그램
future = node.send_request()
rclpy.spin_until_future_complete(node, future)
response = future.result()

# 계속 실행되는 노드
future = self.client.call_async(request)
future.add_done_callback(self.on_response)

def on_response(self, future):
    try:
        response = future.result()
        self.get_logger().info('서비스 완료')
    except Exception as exc:
        self.get_logger().error(f'서비스 실패: {exc}')

Future는 나중에 도착할 Response의 완료 상태와 결과를 보관한다. Subscription callback 안에서 spin_until_future_complete()로 같은 executor를 다시 기다리면 다른 callback이 실행되지 못해 교착이나 응답 지연이 생길 수 있다. 이런 노드에서는 call_async() 뒤 add_done_callback()을 사용하고, 동시에 여러 요청을 보내지 않도록 pending 상태를 관리한다. 앞서 설명한 Future의 완료 콜백은 응답이 도착할 때 실행된다. Node가 계속 동작해야 한다면 응답을 기다리며 블로킹하기보다 이 방식을 우선 고려한다.

16.2.3 Pose 구독과 SetPen 서비스를 결합하기#

다음 노드는 /turtle1/pose를 계속 구독하고 x=5.5 경계를 넘을 때만 /turtle1/set_pen을 호출한다. 왼쪽에서는 노란색, 오른쪽에서는 초록색 선을 그린다. 같은 영역의 Pose가 반복 수신될 때 Service를 계속 호출하지 않도록 current_region과 pending을 사용한다.

import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose
from turtlesim.srv import SetPen


class RegionPenClient(Node):
    def __init__(self):
        super().__init__('lesson_10_region_pen_client')
        self.client = self.create_client(SetPen, '/turtle1/set_pen')
        self.subscription = self.create_subscription(
            Pose, '/turtle1/pose', self.on_pose, 10)
        self.current_region = None
        self.pending = False

    def on_pose(self, message):
        region = 'left' if message.x < 5.5 else 'right'
        if region == self.current_region or self.pending:
            return
        if not self.client.service_is_ready():
            return

        request = SetPen.Request()
        if region == 'left':
            request.r, request.g, request.b = 255, 255, 0
        else:
            request.r, request.g, request.b = 0, 255, 0
        request.width = 3
        request.off = 0

        self.pending = True
        future = self.client.call_async(request)
        future.add_done_callback(
            lambda done, target=region: self.on_response(done, target))

    def on_response(self, future, target):
        self.pending = False
        try:
            future.result()
            self.current_region = target
        except Exception as exc:
            self.get_logger().error(f'set_pen 실패: {exc}')


def main(args=None):
    rclpy.init(args=args)
    node = RegionPenClient()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

SetPen.Request의 r·g·b는 0~255 색상값, width는 선 굵기, off는 1일 때 펜 끄기다. service_is_ready()는 현재 discovery된 서버가 있는지 즉시 확인한다. pending은 이전 요청의 Response가 오기 전에 다음 요청이 겹치는 것을 막는다. 응답이 성공했을 때만 current_region을 갱신하므로 실패한 요청은 다음 Pose에서 다시 시도할 수 있다.

ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key
# 세 번째 터미널
ros2 run turtle_robot lesson_10_region_pen_client

16.3 Service가 적합하지 않은 경우#

수초 이상 걸리는 이동이나 작업을 Service callback에서 오래 수행하면 client와 executor가 불필요하게 묶인다. 진행률·취소·최종결과가 필요한 작업은 Action으로 설계한다.

17장. Custom msg·srv·action Interface#

17.1 Interface 전용 package#

custom interface는 재사용성을 위해 별도 ament_cmake package에 두는 경우가 많다. .msg/.srv/.action은 Python 코드가 아니라 IDL(Interface Definition Language, 인터페이스 정의 언어) 기반 정의 입력이며 build 과정에서 언어별 타입 코드가 생성된다.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
mkdir -p tutorial_interfaces/msg tutorial_interfaces/srv tutorial_interfaces/action

17.2 msg와 srv 예#

# msg/Num.msg
int64 num

# srv/AddThreeInts.srv
int64 a
int64 b
int64 c
---
int64 sum

17.3 CMake와 package.xml#

find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "srv/AddThreeInts.srv"
)

rosidl_default_generators가 interface 정의를 Python 등 필요한 target language code로 생성한다. package.xml에는 buildtool/export/runtime 관련 rosidl dependency와 interface package group을 선언한다.

17.4 build와 검증#

cd ~/ros2_ws
colcon build --packages-select tutorial_interfaces
source install/setup.bash
ros2 interface show tutorial_interfaces/msg/Num
ros2 interface show tutorial_interfaces/srv/AddThreeInts

interface show에 구조가 보이면 생성·설치·source가 정상이다.

17.5 Action 정의#

# action/Fibonacci.action
int32 order
---
int32[] sequence
---
int32[] partial_sequence

Action은 goal/result/feedback 세 부분이다. 실제 Action server/client 구현은 뒤의 심화 Action 장에서 자세히 다룬다.

18장. Python Parameter·ros2doctor#

Python Node에서 Parameter를 선언하고 CLI(Command-Line Interface, 명령줄 인터페이스)·Launch·YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)로 설정값을 바꾸는 방법, ros2doctor로 환경을 점검하는 방법을 실습한다. 학습자는 실행할 때 바뀌는 설정과 코드 자체를 구분할 수 있어야 한다.

18.1 Python Node에서 Parameter 선언#

self.declare_parameter('my_parameter', 'world')
value = self.get_parameter('my_parameter').value
self.get_logger().info(f'Hello {value}!')

18.2 CLI·Launch·YAML로 Override#

ros2 run my_package my_node --ros-args -p my_parameter:=robot

# YAML 예
my_node:
  ros__parameters:
    my_parameter: robot

parameter는 코드의 default와 배포 설정을 분리한다. CLI -p, launch의 parameters, YAML file이 같은 parameter interface를 통해 값을 주입한다. Override란 코드에서 선언한 기본값 대신 실행 환경의 값을 적용하는 것이다. --ros-args -p는 명령행, YAML은 설정 파일, Launch의 parameters는 실행 구성에서 같은 Parameter를 지정한다.

18.2.1 속도 Parameter를 CLI와 YAML로 운용하기#

고정 숫자로 작성한 turtlesim 속도 Publisher를 Parameter 기반으로 바꾸면 코드를 수정하지 않고 실행 중 속도를 조정할 수 있다. 기본값을 실수로 선언하면 Parameter 타입도 double로 결정되므로 CLI에서도 2가 아니라 2.0처럼 실수 값을 사용한다.

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class CmdVelParameterPublisher(Node):
    def __init__(self):
        super().__init__('lesson_13_cmd_vel_parameters')
        self.declare_parameter('linear_speed', 1.0)
        self.declare_parameter('angular_speed', 0.0)
        self.publisher = self.create_publisher(
            Twist, '/turtle1/cmd_vel', 10)
        self.timer = self.create_timer(0.1, self.publish_velocity)

    def publish_velocity(self):
        message = Twist()
        message.linear.x = float(
            self.get_parameter('linear_speed').value)
        message.angular.z = float(
            self.get_parameter('angular_speed').value)
        self.publisher.publish(message)


def main(args=None):
    rclpy.init(args=args)
    node = CmdVelParameterPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        if rclpy.ok():
            rclpy.shutdown()


if __name__ == '__main__':
    main()

publish_velocity()가 매 주기 현재 Parameter를 읽으므로 ros2 param set의 결과가 다음 Timer callback부터 반영된다. 시작할 때 한 번만 읽어 멤버 변수에 저장하는 구현이라면 실행 중 변경값은 자동 반영되지 않는다. 변경 검증이나 여러 값을 원자적으로 함께 적용해야 할 때는 Intermediate I.2절의 Parameter 변경 검증 callback을 사용한다.

ros2 run turtle_robot lesson_13_cmd_vel_parameters

# 다른 터미널
ros2 param list /lesson_13_cmd_vel_parameters
ros2 param get /lesson_13_cmd_vel_parameters linear_speed
ros2 param describe /lesson_13_cmd_vel_parameters linear_speed
ros2 param set /lesson_13_cmd_vel_parameters linear_speed 2.0
ros2 param set /lesson_13_cmd_vel_parameters angular_speed 1.0

ros2 param describe는 현재 값이 아니라 ParameterDescriptor를 조회한다. 선언된 타입과 설명, 읽기 전용 여부, 정수·실수 범위가 설정되어 있으면 그 제약까지 확인할 수 있다. set하기 전에 describe로 계약을 확인하면 문자열·정수·실수 타입을 잘못 전달하는 실수를 줄일 수 있다. 실행 중 조정한 값을 재사용하려면 dump 결과를 YAML로 저장한다. 최상위 키는 대상 노드의 이름 또는 완전한 이름이며, ros__parameters 아래가 실제 Parameter 사전이다. dump에 포함된 불필요한 항목을 제거하더라도 들여쓰기와 노드 이름은 유지한다.

/lesson_13_cmd_vel_parameters:
  ros__parameters:
    linear_speed: 2.0
    angular_speed: 1.0

config/cmd_vel.yaml을 패키지의 실행 산출물에 포함하려면 setup.py가 YAML을 install 공간으로 복사하도록 등록한다. glob은 config 폴더의 .yaml 파일 목록을 만들고, data_files의 목적 경로는 package share 디렉터리다. setup.py나 data_files를 바꾼 뒤에는 다시 빌드해야 한다.

from glob import glob

data_files=[
    # 기존 항목 유지
    ('share/' + package_name + '/config', glob('config/*.yaml')),
]
cd ~/ros2_ws
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash

ros2 run turtle_robot lesson_13_cmd_vel_parameters   --ros-args   --params-file   ~/ros2_ws/install/turtle_robot/share/turtle_robot/config/cmd_vel.yaml

# 파일 없이 시작값만 바꿀 때
ros2 run turtle_robot lesson_13_cmd_vel_parameters   --ros-args   -p linear_speed:=1.5   -p angular_speed:=0.5

--ros-args는 뒤의 인자를 ROS(Robot Operating System, 로봇 운영체제) 전용 인자로 해석하는 경계다. --params-file은 YAML 파일을 읽고, -p name:=value는 한 Parameter를 명령행에서 덮어쓴다. 소스의 config 경로가 아니라 install 공간의 파일로 실행해 보아야 패키지가 다른 컴퓨터에도 배포 가능한 상태인지 검증할 수 있다. 이 명령을 읽을 때 --ros-args 뒤가 ROS 전용 설정이라는 경계부터 찾는다. 그다음 --params-file 또는 -p의 의미를 확인하면 긴 명령행도 구조가 보인다.

18.3 ros2doctor#

ros2 doctor
ros2 doctor --report

ros2doctor는 ROS 환경, network, middleware, package 상태를 종합적으로 검사한다. 특정 Topic 하나를 분석하는 도구라기보다 설치·환경 전체의 1차 건강검진 도구다.

pluginlib::ClassLoader<Polygon> loader(
"polygon_base", "polygon_base::Polygon");
auto polygon = loader.createSharedInstance("polygons::Square");

18.4 기본 과정 완료 기준#

· Node와 Topic·Service·Action·Parameter를 CLI에서 관찰하고, 각 통신 방식이 적합한 상황을 구분할 수 있다.

· colcon, workspace, underlay/overlay, package와 setup.py의 console_scripts가 각자 맡는 역할을 설명할 수 있다.

· Python rclpy로 Publisher/Subscriber 및 Service Server/Client를 만들고, Timer·Subscription·Service Callback이 언제 실행되는지 설명할 수 있다.

· custom msg/srv/action은 rosidl을 통해 Python에서 import 가능한 타입이 되며, interface 정의를 바꾸면 재빌드해야 한다는 사실을 이해한다.

· Topic 통신에서 Message 타입과 QoS(Quality of Service, 서비스 품질)가 맞아야 하고, Service Client의 Future가 응답을 기다리는 상태를 나타낸다는 사실을 설명할 수 있다.

· Launch는 여러 노드의 실행 구성을 재현하고 rosbag2는 Topic 데이터를 기록·재생한다는 사실을 설명할 수 있다.

여기까지 익혔다면 Python으로 Node·Topic·Service·Action·Parameter를 CLI와 코드에서 다루고, workspace와 package를 직접 구성할 수 있다. 아래 종합 정리로 실행 명령과 개념의 연결을 다시 확인한 뒤 Intermediate로 넘어간다.

18.5 Beginner 종합 정리 — ROS 2(Robot Operating System 2, 로봇 운영체제 2)를 Python으로 연결하기#

Beginner에서는 개별 명령을 외우기보다 한 로봇 시스템을 설명하는 순서를 익혔다. 예를 들어 turtlesim을 띄운 뒤 키보드 명령을 보내고 거북이의 위치를 기록하려면 어떤 Node가 있는지, 어떤 Topic이 연결되는지, 각 메시지의 타입과 QoS가 무엇인지 차례로 조사한다.

핵심 요소를 하나의 그림으로 이해하기#

Node는 기능별 실행 단위다. Publisher는 Topic에 메시지를 보내고, Subscriber는 같은 Topic의 메시지를 받아 Callback을 실행한다. Topic은 연속적인 상태·센서·명령 스트림에 적합하다. 같은 이름이라도 Message 타입이 다르거나 QoS가 호환되지 않으면 의도한 통신이 성립하지 않는다.

Service는 Request를 보내고 Response를 받는 짧은 원격 호출이다. Client의 call_async()는 Future를 반환하므로, Node는 응답을 기다리는 동안 다른 Callback을 처리할 수 있다. Future는 응답이 도착했다는 표식과 그 결과를 담는 객체다. 실행기는 Executor이며, Callback은 데이터가 도착하거나 타이머가 만료되었을 때 Executor가 호출하는 함수다.

Action은 시간이 걸리는 목표 지향 작업을 다룬다. Goal은 시작 지시, Feedback은 진행 상황, Result는 최종 결과이며 작업 취소도 가능하다. Parameter는 Node별 동작 설정값, Launch는 여러 Node와 설정을 재현하는 실행 구성, rosbag2는 Topic 데이터의 기록·재생 장치다. 즉, 통신(Topic·Service·Action), 설정(Parameter), 실행(Launch), 관찰·재현(CLI·rosbag2)이 서로 역할을 나누어 하나의 시스템을 이룬다.

한 번 더 실행하는 CLI 점검 순서#

다른 터미널마다 source /opt/ros/jazzy/setup.bash 또는 source ~/ros2_ws/install/setup.bash를 실행한다. source는 현재 셸의 경로와 환경변수를 바꾸는 동작이며, 한 터미널에서 실행해도 다른 터미널에는 자동 전파되지 않는다.

# 터미널 A: turtlesim 실행
ros2 run turtlesim turtlesim_node

# 터미널 B: 주요 인터페이스 확인
ros2 node list
ros2 topic list -t
ros2 topic info /turtle1/pose
ros2 topic echo /turtle1/pose
ros2 service list -t
ros2 action list -t
ros2 param list /turtlesim

# 터미널 C: 데이터 기록과 재생
ros2 bag record -o beginner_pose /turtle1/pose
ros2 bag info beginner_pose

ros2 node list는 노드를, topic list -t는 Topic과 메시지 타입을 보여 준다. topic info는 Publisher·Subscription 개수를 확인하는 데 쓰고, echo는 메시지 내용을, hz는 수신 주기를 확인한다. service list -t와 action list -t는 요청/작업 인터페이스를, param list는 특정 Node에 속한 설정값을 찾는 명령이다. ros2 bag record는 흐르는 메시지를 저장한다.

Python 노드 개발의 다섯 단계#

① ros2 pkg create --build-type ament_python으로 패키지를 만든다. ② rclpy.node.Node를 상속해 Publisher·Subscription·Service·Timer 중 필요한 인터페이스를 등록한다. ③ setup.py의 console_scripts에 모듈과 main 함수를 연결한다. ④ colcon build --packages-select 패키지명 --symlink-install로 빌드한다. ⑤ source install/setup.bash 뒤 ros2 run 패키지명 실행이름으로 구동한다.

--symlink-install은 Python 소스 수정 시 설치 공간에 심볼릭 링크를 유지하는 옵션이다. 새 console_scripts나 설정 파일 등록처럼 설치 메타데이터를 바꾸면 다시 빌드해야 한다. Python 코드의 create_publisher(MessageType, topic_name, qos_depth)는 메시지 타입, Topic 이름, 큐 깊이를 차례로 받는다는 점을 실제 코드에서 확인한다.

통신방식 선택과 장애 분석 연습#

연습 1. 레이저 스캔을 초당 여러 번 전달한다면 Topic이다. 연습 2. 새 거북이를 한 번 생성하고 생성된 이름을 받는다면 Service다. 연습 3. 일정 거리 이동을 요청하고 진행률을 보면서 필요하면 취소한다면 Action이다. 연습 4. 최대속도 수치를 코드 수정 없이 바꾼다면 Parameter다. 연습 5. 여러 노드를 같은 설정으로 다시 실행한다면 Launch다.

통신이 보이지 않을 때는 순서대로 확인한다. (1) 노드가 실행 중인가? ros2 node list. (2) Topic/Service/Action 이름이 맞는가? 관련 list 명령. (3) 메시지 타입이 같은가? ros2 topic type 또는 ros2 interface show. (4) QoS가 호환되는가? ros2 topic info -v. (5) Terminal의 환경 source와 ROS_DOMAIN_ID가 맞는가? (6) Service Client의 Future를 기다리는 동안 Executor가 응답 Callback을 처리할 수 있는가?

마지막으로 자신의 말로 설명해 보자. "Node가 Topic을 발행하면 구독자의 Callback이 Executor에서 실행된다. Service의 요청 결과는 Future로 받는다. 오래 걸리는 목표는 Action을 사용한다. Parameter로 값을 바꾸고 Launch로 실행구성을 재현한다." 이 흐름을 turtlesim의 실제 노드·Topic 이름에 대입할 수 있으면 Beginner의 핵심을 습득한 것이다.

Part III-A. Intermediate ROS 2 개발#

I.1 rosdep과 package.xml dependency 의미#

rosdep은 package.xml의 의존성 이름을 Ubuntu apt 등 플랫폼별 설치 패키지와 연결한다. ROS 2 Python package도 외부 dependency를 사용하면 package.xml에 선언해야 다른 PC(Personal Computer, 개인용 컴퓨터)에서 재현할 수 있다.

태그 의미
<depend> build와 runtime 모두 필요
<build_depend> build 때만 필요
<build_export_depend> 내가 export한 header/CMake가 다른 package 빌드에 필요
<exec_depend> 실행 시 필요
<test_depend> test 때만 필요
sudo rosdep init     # 시스템에서 최초 한 번
rosdep update        # rosdistro index cache 갱신
rosdep install --from-paths src --ignore-src -r -y

--ignore-src는 현재 workspace source로 이미 제공되는 ROS package를 시스템 패키지로 다시 설치하지 않게 한다. 새로운 PC나 CI에서 재현 가능한 build를 만들려면 import가 되는지만 보지 말고 package.xml dependency가 정확해야 한다.

Client는 create_client<ActionT>()로 server를 찾고 SendGoalOptions에 goal response, feedback, result callback을 연결한다. Python의 ActionClient와 상태모델은 동일하다.

같은 process에 component를 배치하면 process 간 serialization 비용을 줄일 수 있지만 fault isolation은 약해진다. 이 trade-off는 51장에서 성능·격리 관점으로 더 깊게 다룬다.

이 패턴은 library/helper API(Application Programming Interface, 응용 프로그램 인터페이스)가 구체적인 Node class 대신 필요한 기능 계약에 의존하도록 만들어 재사용성을 높인다.

I.2 Parameter 변경 감시 — 로컬 검증과 전역 이벤트를 구분한다#

parameter 변경을 다루는 방식은 두 가지다. 자신의 Node가 값을 받아들이기 전에 검사하려면 on-set callback을 사용하고, 다른 Node를 포함해 발생한 parameter event를 관찰하려면 ParameterEventHandler를 사용한다.

# Python: 자신의 parameter 변경 검증
from rcl_interfaces.msg import SetParametersResult

def validate(params):
    for param in params:
        if param.name == 'max_speed' and param.value <= 0:
            return SetParametersResult(successful=False, reason='must be > 0')
    return SetParametersResult(successful=True)

self.add_on_set_parameters_callback(validate)
# Python: parameter event 구독
from rclpy.parameter_event_handler import ParameterEventHandler
self.handler = ParameterEventHandler(self)
self.cb_handle = self.handler.add_parameter_callback(
    parameter_name='max_speed',
    node_name='/controller',
    callback=lambda p: self.get_logger().info(str(p.value)))

ParameterEventHandler에서 등록한 Callback은 Handler 객체와 Callback 핸들을 유지해야 계속 호출된다. 이 점은 Timer나 Subscription 객체를 Node에 보관하는 것과 같은 수명 관리 원칙이다.

I.3 Launch Intermediate — Substitution과 Event Handler#

Launch argument는 즉시 Python 변수로 확정되는 값이 아니라 launch 실행 context에서 평가될 수 있다. LaunchConfiguration, EnvironmentVariable, FindPackageShare, PathJoinSubstitution 같은 substitution은 재사용 가능한 launch 구성을 만든다.

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

robot_ns = LaunchConfiguration('robot_ns')
node = Node(package='demo_nodes_cpp', executable='talker', namespace=robot_ns)

return LaunchDescription([
    DeclareLaunchArgument('robot_ns', default_value='robot1'),
    node,
    RegisterEventHandler(OnProcessExit(target_action=node, on_exit=[])),
])

Substitution은 실행 시점 값 치환, Event Handler는 process start/exit·shutdown 같은 event에 따라 후속 action을 실행하는 규칙이다. 큰 시스템에서는 IncludeLaunchDescription으로 하위 launch를 나누고 namespace·parameter file·remapping을 상위에서 주입한다.

I.4 tf2·Testing·URDF(Unified Robot Description Format, 통합 로봇 기술 형식)·RViz는 기존 심화부로 연결한다#

공식 Intermediate 영역 기존 교재가 다루는 깊이 연결 위치
tf2 SE(2)/SE(3), buffer/time, static/dynamic TF, Point/Pose transform, Hand-Eye까지 확장 26장·31장
Testing unit/integration/launch_testing, rosbag fixture, CI까지 확장 45장·55장
URDF link/joint, Xacro, robot_state_publisher, ros2_control/MoveIt 연계 27장·28장
RViz Fixed Frame, TF/Path/Costmap/Marker, Navigation/Manipulation 디버깅 24장·27장·31장·36장

따라서 이 네 영역은 Intermediate 튜토리얼을 다시 옮기기보다 기존 심화 장을 읽는 편이 중복이 적고 학습 깊이도 높다.

I.5 Intermediate 완료 체크#

· package.xml dependency 태그와 rosdep의 역할을 설명할 수 있다.

· Python에서 Action Client의 Goal·Feedback·Result·Cancel 흐름과 Future의 완료 시점을 구분할 수 있다.

· Python Executor가 Timer·Subscription·Service Callback을 처리한다는 점을 설명할 수 있다.

· Callback Group 설정이 필요한 이유와 동기 대기에서 생기는 교착 위험을 설명할 수 있다.

· parameter 변경 검증 callback과 ParameterEventHandler의 차이를 설명할 수 있다.

· LaunchConfiguration/substitution/event handler를 사용해 재사용 가능한 launch 구성을 만들 수 있다.

· tf2·Testing·URDF·RViz는 뒤의 심화 장에서 실제 로봇 문맥으로 확장할 수 있다.

Part IV. Concepts & Development Deep Dive#

19장. ROS 2를 분산 시스템으로 이해하기#

이 장을 끝내면 실행 중인 Graph를 읽어 Node·Topic·Service·Action의 역할을 설명한다.

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

19.1 ROS 2는 로봇 제어 프로그램이 아니라 연결 규칙이다#

독자의 첫 질문 ROS 2를 로봇을 움직이는 단일 라이브러리로 생각하면 뒤에서 계속 혼란이 생긴다. ROS 2는 센서·인식·계획·제어 프로그램이 서로 발견되고 데이터를 교환하도록 만드는 통신·실행 프레임워크다. 실제 모터 구동은 드라이버나 controller가 담당하고, ROS 2는 그 기능들을 연결한다.

새 시스템을 볼 때는 소스코드보다 먼저 어떤 노드가 있고 무엇을 발행하며 무엇을 요청받는가를 본다. 카메라 노드는 영상을 발행하고, 인식 노드는 영상을 구독해 검출 결과를 발행하며, 상위 행동 노드는 그 결과를 받아 이동 Action을 보낼 수 있다. 이 연결관계가 ROS Graph다.

ROS 2는 센서, 인식, 계획, 제어, 기록, 시각화를 하나의 거대한 실행파일에 넣지 않고 여러 기능 단위로 나누어 연결하도록 만든다. 이때 중요한 것은 “노드를 많이 만드는 것” 자체가 아니라 각 기능 사이의 경계를 명시적으로 만드는 것이다. 경계가 명확하면 카메라 드라이버를 바꾸거나 시뮬레이터를 실제 로봇으로 교체해도 상위 로직을 재사용하기 쉬워진다.

그림 19-1. 같은 시스템을 코드 파일이 아니라 ROS Graph로 읽는다.

실무에서 새로운 로봇을 받았을 때 가장 먼저 API 문서를 처음부터 읽을 필요는 없다. 실행시킨 뒤 `ros2 node list`, `ros2 topic list -t`, `ros2 service list -t`, `ros2 action list -t`를 보면 시스템이 어떤 계약을 외부로 드러내는지 빠르게 알 수 있다. 이것이 ROS 2의 강점이다.

19.2 ROS(Robot Operating System, 로봇 운영체제) Graph와 프로세스의 차이#

바로 확인 talker/listener를 실행한 상태에서 `ros2 node list`와 `ros2 node info /talker`를 실행한다. 프로세스와 ROS Graph의 논리적 Node가 같은 개념이 아니라는 것을 확인한다.

운영체제 프로세스와 ROS Node를 분리해서 이해하면 composition이 쉬워진다. 하나의 프로세스에 여러 Node를 넣으면 전달 비용을 줄일 수 있지만 한 프로세스의 실패가 여러 Node에 영향을 줄 수 있다. 반대로 프로세스를 분리하면 격리와 재시작은 쉬워지지만 통신 비용이 늘 수 있다.

Node는 ROS Graph에 참여하는 논리적 기능 단위다. 프로세스는 운영체제가 관리하는 실행 단위다. 초반에는 “노드 하나 = 프로세스 하나”로 생각해도 큰 문제는 없지만, 실제로는 하나의 프로세스 안에 여러 Node를 넣을 수 있고, 반대로 하나의 기능을 여러 Node가 협력해 구현할 수도 있다. 이 구분은 나중에 composition, executor, 성능 최적화를 이해할 때 중요해진다.

ros2 node list
ros2 node info /camera_node
ros2 topic list -t
ros2 service list -t
ros2 action list -t
rqt_graph

관찰 포인트 `node info`는 특정 노드가 publish/subscribe/service/action 중 무엇을 제공하는지 보여준다. 코드가 없어도 노드의 역할을 역으로 추론할 수 있다.

19.3 ROS 2의 계층: rclpy에서 DDS(Data Distribution Service, 데이터 분산 서비스)까지#

이 책의 Python Node는 `rclpy` API(Application Programming Interface, 응용 프로그램 인터페이스)로 Publisher를 만들지만, 메시지가 곧바로 소켓으로 전송되는 것은 아니다. 호출은 공통 C 계층 `rcl`과 RMW(ROS Middleware Interface, ROS 미들웨어 인터페이스)(ROS Middleware) 추상화를 거쳐 선택된 DDS 계열 미들웨어에 전달되고, 그곳에서 discovery·직렬화·전송을 처리한다. 따라서 프로세스 내부 함수 호출과 달리 QoS(Quality of Service, 서비스 품질), 패킷 손실, 네트워크 설정, 시계 차이에 따라 통신이 달라질 수 있다. 초반에는 DDS API를 외우기보다 문제가 어느 계층에서 생겼는지 구분한다.

층 역할 학습 초점
Application 내가 작성한 Node callback과 인터페이스
rclpy/rclcpp 언어별 ROS API Node/Publisher/Action 등
rcl 공통 ROS client library 대부분 간접 사용
RMW 미들웨어 추상화 구현체 교체 가능성
DDS/RTPS 계열 discovery·전송·QoS 네트워크와 QoS 이해

19.4 데이터 흐름과 명령 흐름을 분리한다#

Topic/Service/Action을 고르는 기준은 문법이 편한지가 아니라 정보의 수명주기다. 센서 샘플은 계속 흘러가며 특정 수신자가 없어도 생산될 수 있다. Service는 짧은 질의에 답하고, Action은 작업이 시작된 뒤 피드백·완료·취소라는 상태를 가진다.

판단 연습 카메라 영상→Topic, 일회성 설정 조회→Service, 15초짜리 로봇 이동→Action, 속도 제한값→Parameter로 생각해 본다.

카메라 영상, LiDAR scan, joint state처럼 계속 생성되는 정보는 “흐르는 데이터”다. 반면 “그리퍼를 닫아라”, “목표 위치로 이동하라”는 작업 요청이다. 이 둘을 같은 Topic으로 억지로 표현하면 완료 여부, 오류, 취소, 진행상황을 표현하기 어려워진다. ROS 2는 Topic, Service, Action을 구분해 이런 의미 차이를 인터페이스 수준에서 표현한다.

상황 권장 인터페이스 이유
카메라/라이다/상태 스트림 Topic 생산자와 소비자가 느슨하게 연결되고 지속적으로 데이터가 흐름
짧은 질의·설정 변경 Service request-response가 명확하고 빠르게 끝남
이동·trajectory·장시간 작업 Action goal/feedback/result/cancel 수명주기를 표현
튜닝값·설정값 Parameter 코드와 설정 분리

19.5 분산 시스템으로 생각해야 하는 이유#

ROS 2에서는 코드가 정상이어도 Publisher와 Subscriber가 다른 namespace를 바라보거나 메시지 타입·QoS가 맞지 않으면 통신할 수 없다. TF(Transform, 좌표변환)가 있어도 센서 timestamp와 맞지 않을 수 있고, Lifecycle state·네트워크 discovery가 원인일 수도 있다. 오류를 좁힐 때에는 Graph와 endpoint → 메시지 타입·QoS → frame·time → parameter·lifecycle·network → 코드 순으로 실제 실행 상태를 조사한다.

장 핵심 정리#

  • ROS 2의 기본 단위는 파일이 아니라 실행 중인 Node와 인터페이스다.

  • ROS Graph를 먼저 관찰하면 구현을 몰라도 시스템 구조를 추론할 수 있다.

  • Topic/Service/Action은 문법 차이가 아니라 작업 의미의 차이다.

  • ROS 2 문제는 코드·네트워크·시간·QoS·frame 등 여러 층에서 발생한다.

확인 문제#

  1. Node와 프로세스가 같은 개념이 아닌 이유를 설명하라.

  2. 센서 스트림과 장시간 명령을 같은 Topic으로 처리하면 어떤 문제가 생기는가?

  3. 새 로봇 시스템을 처음 관찰할 때 사용할 CLI(Command-Line Interface, 명령줄 인터페이스) 순서를 적고 각 명령에서 무엇을 확인할지 설명하라.

20장. Package 구조·빌드 체계·개발환경 진단#

20.1 Ubuntu 24.04 + ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy + WSL2(Windows Subsystem for Linux 2, Windows용 Linux 하위 시스템 2)의 역할#

WSL2는 ROS 2 코드 작성과 colcon 빌드, RViz2, 일반적인 Gazebo 학습에는 충분히 실용적이다. 다만 실제 LiDAR·RealSense·USB serial을 연결하거나 회사/로봇 네트워크의 multicast discovery를 다룰 때는 Windows↔︎WSL 경계가 추가된다. 문제를 만났을 때 ROS 문제인지 WSL 네트워크/USB 문제인지 분리할 수 있어야 한다.

20.2 Shell, 환경변수, source#

`source`는 설치 작업이 아니라 현재 shell 프로세스의 환경을 바꾸는 명령이다. 한 터미널에서 source했다고 다른 터미널까지 자동으로 적용되지 않는다. 새 터미널을 열 때마다 `/opt/ros/jazzy/setup.bash`와 필요하면 workspace의 `install/setup.bash`를 읽혀야 한다.

실전 진단 `which ros2`, `printenv ROS_DISTRO`, `echo $AMENT_PREFIX_PATH`를 보면 지금 터미널이 어떤 ROS(Robot Operating System, 로봇 운영체제)와 workspace를 보고 있는지 빠르게 확인할 수 있다.

source /opt/ros/jazzy/setup.bash
source ~/ros2_ws/install/setup.bash
printenv ROS_DISTRO
printenv AMENT_PREFIX_PATH

빌드했는데 예전 코드가 실행되거나 package를 못 찾을 때는 거의 항상 “어느 workspace가 source되어 있는가”를 확인해야 한다. 여러 workspace를 동시에 쓰면 overlay 순서가 더 중요해진다.

20.3 Workspace의 네 폴더#

`src`는 사람이 관리하는 원본이고, `build/install/log`는 빌드 시스템이 만든 산출물이다. Git에는 보통 package 소스만 넣고 build/install/log는 제외한다. 이 구분을 지키면 빌드가 꼬였을 때 산출물을 지우고 재생성해도 원본을 잃지 않는다.

폴더 의미 직접 편집?
src 패키지 소스 예
build 컴파일/빌드 중간물 아니오
install 실행 가능한 설치 결과 아니오
log colcon 로그 문제 분석 시 읽음
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build
source install/setup.bash

20.4 Package는 의존성·배포 경계다#

Package 경계는 폴더 정리보다 중요하다. 메시지 정의, 하드웨어 드라이버, 알고리즘, 상위 애플리케이션을 적절히 분리하면 서로 다른 팀·언어·배포 주기로 관리할 수 있다. 반대로 모든 코드를 한 package에 넣으면 의존성이 엉키고 작은 변경에도 전체를 다시 검증해야 한다.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0 \
  --dependencies rclpy std_msgs learning_ros2

20.5 package.xml, setup.py, CMakeLists.txt를 읽는 순서#

처음 보는 package를 읽을 때는 구현보다 metadata가 빠른 지도다. `package.xml`은 빌드/실행 의존성을, `setup.py` 또는 `CMakeLists.txt`는 무엇을 설치하고 어떤 executable을 노출하는지 말해 준다. 이 세 파일을 먼저 읽으면 package 역할을 빠르게 추정할 수 있다.

20.6 colcon build와 선택 빌드#

20.6.1 colcon은 컴파일러가 아니라 workspace 빌드 지휘자다#

`colcon`은 CMake나 Python 빌드 도구 자체를 대체하지 않는다. 하나의 workspace 안에 여러 ROS 2 package가 있을 때, 각 package의 의존관계를 파악하고 필요한 순서대로 해당 package의 실제 빌드 도구를 호출하는 meta-build/orchestration 도구다. 메시지 정의 패키지는 주로 CMake/ament_cmake를, Python package라면 setuptools/ament_python을 통해 실제 설치 작업이 이루어진다.

핵심 구분 컴파일은 compiler와 CMake가 하고, Python package 설치는 setuptools가 한다. ament는 ROS 2 package용 빌드·설치 규칙을 제공하고, colcon은 workspace 전체를 묶어 어떤 package를 어떤 순서로 빌드·테스트할지 조율한다.

20.6.2 여러 package의 의존순서를 자동으로 처리한다#

다음과 같은 workspace를 생각해 보자. `robot_app`이 `perception`을 사용하고, `perception`이 `my_interfaces`의 custom message를 사용한다면 `robot_app`을 먼저 빌드할 수 없다. colcon은 package metadata를 이용해 의존관계를 보고 선행 package부터 처리한다.

ros2_ws/
└── src/
    ├── my_interfaces/
    ├── camera_driver/
    ├── perception/
    └── robot_app/

의존관계 예:
my_interfaces
      ↓
camera_driver
      ↓
perception
      ↓
robot_app

workspace 루트에서 `colcon build`를 실행하면 package를 이름순으로 무작정 빌드하는 것이 아니라 의존성을 고려해 처리한다. 특정 package와 그 의존 package까지 빌드하고 싶다면 `--packages-up-to`가 유용하다.

20.6.3 빌드했다고 바로 ros2 run에서 보이는 것은 아니다#

`colcon build`가 끝나면 결과는 `install/` 아래에 만들어진다. 현재 shell은 그 위치를 자동으로 알지 못하므로 overlay 환경을 읽혀야 한다.

cd ~/ros2_ws
colcon build --symlink-install

source install/setup.bash

ros2 pkg list | grep my_package
ros2 run my_package my_node

개발 흐름은 ‘소스 작성 → colcon build → install 생성 → setup.bash source → ros2 run’이다. 새 터미널을 열었다면 `/opt/ros/jazzy/setup.bash`와 workspace의 `install/setup.bash`를 다시 source해야 한다.

일반 설치에서는 package 파일이 `install/` 공간으로 복사될 수 있다. `--symlink-install`을 사용하면 가능한 파일은 원본 소스와 install 공간을 symbolic link로 연결해 개발 중 수정 반영을 빠르게 한다. Python source만 수정했을 때 매번 전체 복사 설치를 반복하지 않아도 되는 경우가 많다.

다시 빌드가 필요한 경우 `setup.py`, `package.xml`, CMake 설정, entry point, interface(.msg/.srv/.action), launch/config 설치 규칙을 바꾸었다면 다시 `colcon build`하는 습관을 유지한다.

20.6.5 전체 workspace 대신 필요한 package만 빌드한다#

# package 목록 확인
colcon list

# 특정 package만
colcon build --packages-select my_package

# my_app과 my_app이 필요로 하는 dependency까지
colcon build --packages-up-to my_app

# 특정 package만 test
colcon test --packages-select my_package
colcon test-result --verbose

workspace가 커질수록 선택 빌드가 중요해진다. `--packages-select`는 지정 package만 대상으로 삼고, `--packages-up-to`는 지정 package와 그 의존 package까지 포함한다. 처음 빌드하거나 의존 package까지 갱신해야 한다면 `--packages-up-to`가 더 안전하다.

20.6.6 CMake·setuptools·ament·colcon·rosdep의 관계#

도구 역할 대표 사용
CMake C/C++ project의 compile/link/install 구성 C++ node·library
setuptools Python package 설치와 console entry point ament_python package
ament_cmake / ament_python ROS 2 package 규칙·설치·테스트 helper package build type
colcon workspace 여러 package의 build/test 순서와 실행 조율 colcon build/test
rosdep package.xml을 바탕으로 외부/system dependency 설치 apt 등 의존성 준비

`rosdep`과 `colcon`도 역할이 다르다. rosdep은 빌드에 필요한 외부 라이브러리가 준비되었는지를 해결하고, colcon은 준비된 환경에서 workspace package들을 어떤 순서로 빌드·테스트할지를 해결한다.

20.6.7 전체 흐름을 한 번에 보기#

package.xml
   │
   ├─ rosdep ─────────→ 외부 시스템 의존성 준비
   │
   └─ package dependency 정보
              ↓
           colcon
              ↓
     package 의존순서 계산
              ↓
   ┌──────────┴──────────┐
ament_cmake          ament_python
   ↓                     ↓
 CMake                setuptools
   └──────────┬──────────┘
              ↓
      build/ + install/ + log/
              ↓
     source install/setup.bash
              ↓
        ros2 run / ros2 launch

한 문장으로 colcon은 ROS 2 workspace 전체를 빌드하는 ‘지휘자’다. 직접 컴파일하기보다 각 package의 빌드 도구를 올바른 순서로 호출하고 build/test/install 과정을 workspace 단위로 관리한다.

이제 실제 명령을 적용한다. `colcon list`로 발견된 package를 확인하고, `colcon build --symlink-install`로 전체를 빌드한다. workspace가 커지면 `--packages-select`와 `--packages-up-to`를 사용해 범위를 줄인다.

cd ~/ros2_ws
colcon list
colcon build --symlink-install
colcon build --packages-select learning_ros2
colcon build --packages-up-to my_app

20.7 빌드 실패를 읽는 법#

빌드 로그는 맨 마지막 줄보다 처음 실패한 package와 최초 error를 찾는 것이 중요하다. 이후 package의 실패는 첫 실패 때문에 연쇄적으로 발생한 결과일 수 있다. Python import 오류, package.xml 누락, CMake target/install 누락, source 안 된 overlay를 서로 다른 층의 문제로 분리한다.

  1. 가장 먼저 실패한 package를 찾는다. 뒤의 연쇄 실패 메시지는 원인보다 결과일 수 있다.

  2. `rosdep install --from-paths src --ignore-src -r -y`로 시스템 의존성을 점검한다.

  3. package.xml 의존성과 import/header 사용이 일치하는지 본다.

  4. 빌드 환경이 꼬였으면 해당 package만 `build/`, `install/`에서 정리한 뒤 재빌드한다. 전체 삭제는 마지막 수단이다.

장 핵심 정리#

  • source는 현재 shell이 어떤 ROS 설치/overlay를 보는지 결정한다.

  • Package는 코드 묶음이 아니라 의존성과 설치 규칙을 가진 배포 단위다.

colcon은 compiler 자체가 아니라 workspace 여러 package의 의존관계를 계산하고 각 package의 빌드 도구를 호출하는 orchestration 도구다.

  • 새 패키지를 읽을 때 package.xml → build/install 정의 → 실행 entry point → 코드 순으로 보면 빠르다.

확인 문제#

  1. underlay와 overlay를 설명하고 source 순서가 왜 중요한지 설명하라.

  2. `colcon build`가 어떤 계층의 도구인지 설명하고, build 성공 후 새 executable이 `ros2 run`에서 보이지 않을 때 확인할 항목을 세 가지 적어라.

  3. interface package를 실행 노드 package와 분리하는 장점을 설명하라.

21장. Node·Topic·Message·QoS·Custom Interface#

이 장을 끝내면 publisher/subscriber를 작성하고 message 타입과 QoS 문제를 CLI(Command-Line Interface, 명령줄 인터페이스)로 진단한다.

약어 표기 QoS: Quality of Service(서비스 품질)

21.1 Node의 생명주기와 callback#

Node는 이벤트가 들어올 때 callback을 실행하는 객체다. `spin()`이 실행되는 동안 subscriber 메시지, timer, service 요청, action 이벤트가 준비되면 executor가 callback을 호출한다. 그래서 callback 안에서 오래 계산하거나 sleep하면 다른 이벤트 처리도 밀릴 수 있다.

코드 읽는 법 노드를 보면 먼저 `create_publisher`, `create_subscription`, `create_timer`, `create_service`, `ActionServer`를 찾는다. 이것만 표시해도 외부 인터페이스와 실행 주기를 대부분 파악할 수 있다.

일반적인 rclpy 노드는 `rclpy.init()`으로 통신 기반을 초기화하고, Node 객체를 만든 뒤, `rclpy.spin()`으로 이벤트를 기다린다. Subscriber 메시지 도착, Timer 만료, Service 요청 등은 callback으로 처리된다. 즉 ROS 2 노드는 “메인 루프에서 모든 것을 순서대로 실행하는 프로그램”이라기보다 이벤트가 도착할 때 반응하는 컴포넌트다.

import rclpy
from rclpy.node import Node

class Heartbeat(Node):
    def __init__(self):
        super().__init__('heartbeat')
        self.timer = self.create_timer(1.0, self.tick)

    def tick(self):
        self.get_logger().info('alive')

def main():
    rclpy.init()
    node = Heartbeat()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

21.2 Topic은 비동기 데이터 채널이다#

Publisher가 `publish()`를 호출했다고 subscriber 함수가 같은 call stack에서 즉시 실행되는 것은 아니다. 메시지는 미들웨어를 거쳐 전달되고 subscriber callback은 자신의 executor 스케줄에 따라 실행된다. 이 비동기성이 생산자·소비자를 느슨하게 결합하지만 동시에 지연·queue·QoS를 고려하게 만든다.

Publisher는 특정 Topic 이름과 Message 타입으로 샘플을 발행하고, Subscriber는 해당 Topic을 구독한다. Publisher는 Subscriber의 수나 구현을 몰라도 된다. 이 decoupling 덕분에 하나의 센서 데이터를 기록 노드, 시각화 노드, 인식 노드가 동시에 사용할 수 있다.

# 터미널 1
ros2 run demo_nodes_py talker

# 터미널 2
ros2 topic list -t
ros2 topic info /chatter -v
ros2 topic echo /chatter
ros2 topic hz /chatter

21.3 Message 타입을 먼저 확인한다#

Message 타입은 데이터 스키마다. `LaserScan`을 ranges 배열로만 보면 angle_min, angle_increment, frame_id, timestamp를 놓친다. 로봇에서는 숫자값 못지않게 단위·좌표계·시간이 의미를 결정하므로, 처음 보는 Topic은 `ros2 interface show`로 타입부터 읽는 습관이 중요하다.

ROS 2에서 Topic 이름만 맞는다고 통신하는 것이 아니다. 타입이 동일해야 하고, QoS도 호환되어야 한다. 따라서 새로운 Topic을 보면 `ros2 topic type`과 `ros2 interface show`로 구조를 먼저 확인한다. 예를 들어 `sensor_msgs/msg/LaserScan`은 단순 거리 배열이 아니라 각도 시작값, 각도 간격, 최소/최대 range, timestamp가 함께 있는 구조다. 이런 metadata를 무시하면 좌표계와 시간 해석이 틀어진다.

ros2 topic type /scan
ros2 interface show sensor_msgs/msg/LaserScan
ros2 interface show geometry_msgs/msg/Twist

21.4 Publisher와 Subscriber Python 예제#

예제를 그대로 실행한 뒤 publisher 주기, topic 이름, QoS depth를 하나씩 바꿔 본다. 수정→빌드→source→실행의 반복을 몸에 익히는 것이 목적이다. 한 번 돌아가는 것보다 변경이 Graph와 데이터율에 어떻게 반영되는지를 보는 편이 중요하다.

from std_msgs.msg import String

class StatusPub(Node):
    def __init__(self):
        super().__init__('status_pub')
        self.pub = self.create_publisher(String, '/robot_status', 10)
        self.timer = self.create_timer(0.5, self.publish)
        self.count = 0

    def publish(self):
        msg = String()
        msg.data = f'ready {self.count}'
        self.pub.publish(msg)
        self.count += 1

class StatusSub(Node):
    def __init__(self):
        super().__init__('status_sub')
        self.sub = self.create_subscription(
            String, '/robot_status', self.on_msg, 10)

    def on_msg(self, msg):
        self.get_logger().info(msg.data)

Publisher 객체 해설 create_publisher()의 반환값은 publisher handle이며 보통 self.pub처럼 Node 객체가 보관한다. 마지막 10은 단순 배열 크기가 아니라 기본 QoS의 KEEP_LAST depth다. subscriber가 처리하지 못할 정도로 입력이 빠르면 오래된 샘플이 queue에 남아 지연으로 보일 수 있다.

마지막 인자 `10`은 단순 버퍼 숫자처럼 보이지만 실제로는 기본 QoS profile의 history depth에 해당한다. 고속 센서 데이터에서는 이 값이 크다고 항상 좋은 것이 아니다. 처리속도가 입력속도보다 느리면 오래된 샘플이 쌓여 “지연된 최신 데이터”를 처리하게 될 수 있다.

21.5 QoS는 통신 계약이다#

QoS는 publisher가 제공하는 성질과 subscriber가 요구하는 성질의 조합이다. subscriber가 RELIABLE을 요구하지만 publisher가 BEST_EFFORT만 제공하면 연결이 성립하지 않을 수 있다. 반대로 센서 스트림에서는 한 프레임 재전송보다 최신 데이터가 더 중요해 BEST_EFFORT가 합리적일 수 있다.

진단 순서 Topic이 보이는데 `echo`가 안 되면 ① 타입 ② publisher/subscriber 수 ③ `ros2 topic info -v`의 QoS ④ namespace/remap을 순서대로 본다.

QoS(Quality of Service)는 네트워크 품질 설정 정도가 아니라 Publisher와 Subscriber가 어떤 전달 성질을 요구하는지 표현하는 계약이다. 대표적으로 Reliability, Durability, History, Depth, Deadline, Lifespan 등이 있다. ROS 2에서는 Publisher가 제공하는 QoS와 Subscriber가 요구하는 QoS가 호환되어야 실제 연결이 성립한다.

정책 대표 값 로봇에서의 의미
Reliability RELIABLE / BEST_EFFORT 재전송 보장과 지연/최신성 사이의 선택
Durability VOLATILE / TRANSIENT_LOCAL 늦게 접속한 구독자에게 과거 샘플 제공 여부
History KEEP_LAST / KEEP_ALL 과거 샘플 보관 방식
Depth 정수 KEEP_LAST에서 유지할 샘플 수
Deadline 시간 새 샘플이 와야 하는 기대 주기
Lifespan 시간 오래된 샘플의 유효기간

센서 QoS 직관 카메라 30 fps에서 한 프레임을 잃었다고 재전송을 기다리는 동안 새 프레임이 여러 장 도착할 수 있다. 이런 스트림에서는 BEST_EFFORT가 더 자연스러운 경우가 많다. 반대로 중요 상태 이벤트나 명령은 RELIABLE이 적합할 수 있다.

QoS 불일치 진단#

ros2 topic info /camera/image_raw -v
ros2 topic echo /camera/image_raw --qos-reliability best_effort

QoS CLI 해설 --qos-reliability best_effort는 CLI subscriber가 사용할 QoS reliability를 명시적으로 바꾼다. 센서 publisher가 BEST_EFFORT인데 echo 쪽이 호환되지 않는 정책을 요구하면 Topic 이름과 타입이 맞아도 데이터가 보이지 않을 수 있다.

Topic 이름과 타입이 맞는데 데이터가 보이지 않으면 QoS를 의심한다. 특히 실제 센서 드라이버는 sensor data profile을 쓰는 경우가 많다.

21.6 Custom Interface: 문자열을 구조로 바꾸기#

Custom Interface는 문자열을 예쁘게 만드는 기능이 아니라 시스템 계약을 타입으로 고정하는 일이다. `.msg/.srv/.action`이 바뀌면 생성 코드가 다시 만들어져야 하므로 재빌드가 필요하다. 여러 package가 같은 interface package에 의존하도록 분리하면 타입 버전을 체계적으로 관리할 수 있다.

여러 값을 문자열 하나로 합치는 것은 빠른 데모에는 편하지만 장기적으로는 타입 안전성, 자동완성, 검증, 기록 분석을 망친다. 의미 있는 데이터 구조는 `.msg`, 짧은 요청/응답은 `.srv`, 장시간 작업은 `.action`으로 정의한다.

# RobotStatus.msg
string name
float32 battery
bool ready
geometry_msgs/Pose pose

Interface package는 일반적으로 `ament_cmake`와 `rosidl_default_generators`를 사용한다. 한번 생성된 타입은 Python에서 import해 사용할 수 있다. 이는 ROS 2(Robot Operating System 2, 로봇 운영체제 2)가 언어 간 시스템을 구성하는 중요한 이유 중 하나다.

21.7 메시지 설계 원칙#

메시지에는 지금 필요한 값만 넣는 것이 아니라 장기 유지보수까지 고려한다. frame/time이 필요한 데이터에는 Header를 넣고, ROS 표준 메시지와 단위를 재사용하며, 서로 다른 주기로 변하는 정보를 하나의 거대한 메시지로 묶지 않는다.

  • 단위를 명확히 한다. 위치는 meter, 각도는 radian 등 ROS(Robot Operating System, 로봇 운영체제) 표준 관례를 따른다.

  • 가능하면 표준 메시지 타입을 재사용한다. Pose, Twist, Header를 새로 복제하지 않는다.

  • 시간과 frame이 의미에 중요하면 `std_msgs/Header`를 포함한다.

  • 한 메시지에 서로 다른 수명주기의 데이터를 억지로 묶지 않는다.

  • 고빈도 raw data와 저빈도 semantic result는 별도 Topic으로 나누는 편이 좋다.

장 핵심 정리#

  • Topic은 데이터 스트림, Message는 그 데이터의 타입 계약이다.

  • QoS 불일치만으로도 이름과 타입이 같은 Topic이 연결되지 않을 수 있다.

  • 센서 데이터는 최신성이 완전전달보다 중요할 수 있다.

  • Custom Interface는 시스템의 의미를 타입에 담는 작업이다.

확인 문제#

  1. 카메라 Topic에서 RELIABLE보다 BEST_EFFORT가 적합할 수 있는 이유를 설명하라.

  2. Header가 필요한 메시지와 필요하지 않은 메시지의 예를 각각 들어라.

  3. 문자열 기반 “x=1.2,y=3.4” 메시지보다 구조화 msg가 좋은 이유를 세 가지 적어라.

22장. Service·Action·Parameter·Launch·Lifecycle#

이 장을 끝내면 짧은 RPC와 장시간 작업 인터페이스를 직접 구분하고 구현한다.

22.1 Service: 짧고 완료가 빠른 RPC#

Service는 호출자가 응답을 기다리는 의미가 강하므로 짧고 경계가 분명한 작업에 적합하다. 서버 callback에서 모터가 실제로 10초 움직일 때까지 기다리게 만들면 client와 executor가 함께 묶일 수 있다. 진행상황과 취소가 필요한 작업은 Action으로 설계한다.

Service는 Client가 Request를 보내고 Server가 Response를 반환하는 구조다. 설정값 조회, 장비 상태 읽기, 짧은 계산처럼 호출이 빠르게 끝나는 작업에 적합하다. Service callback 안에서 수초 이상 blocking 작업을 수행하면 executor가 다른 callback을 처리하지 못할 수 있으므로 장시간 작업은 Action으로 설계한다.

ros2 service list -t
ros2 interface show example_interfaces/srv/AddTwoInts
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 2, b: 3}"

22.2 Action: 작업의 수명주기를 표현한다#

Action에는 goal handle이 있고 각 goal은 accepted/executing/succeeded/canceled/aborted 같은 상태를 가진다. Feedback은 중간 진행상황, Result는 최종 결과다. Nav2 이동과 MoveIt trajectory가 Action인 이유는 작업 시간이 길고 취소·preemption이 실제 요구이기 때문이다.

Action은 Goal을 받고, 처리 중 Feedback을 보내며, 끝나면 Result를 반환한다. 필요하면 Cancel도 지원한다. Nav2의 NavigateToPose, MoveIt2의 trajectory 수행 같은 기능이 Action과 잘 맞는 이유는 “끝날 때까지 시간이 걸리고, 진행 중 상태와 취소가 필요하기 때문”이다.

ros2 action list -t
ros2 action info /navigate_to_pose
ros2 action send_goal /fibonacci example_interfaces/action/Fibonacci "{order: 8}" --feedback

Action을 “오래 걸리는 Service”라고만 생각하면 부족하다. Server는 Goal을 accept/reject할 수 있고, goal마다 상태가 존재한다. Client는 future/callback 형태로 결과를 비동기적으로 받을 수 있다. 이 비동기성을 이해해야 GUI나 상위 task manager가 로봇 움직임 동안 멈추지 않는다.

22.3 Parameter: 알고리즘과 튜닝값을 분리한다#

Parameter는 런타임 설정값을 코드 밖으로 드러낸다. 모든 변수를 parameter로 만들 필요는 없다. 운영 중 튜닝하거나 장비별로 바뀌는 값, launch/YAML로 배포해야 하는 값을 parameter로 만들고 내부 구현 세부값은 코드 안에 두는 편이 인터페이스가 명확하다.

self.declare_parameter('max_speed', 0.5)
max_speed = self.get_parameter('max_speed').value

# CLI
ros2 param list
ros2 param get /my_node max_speed
ros2 param set /my_node max_speed 0.3

Nav2와 MoveIt2처럼 파라미터가 많은 시스템에서는 YAML 파일이 사실상 시스템 설정 문서 역할을 한다. 파라미터 이름만 복사해 쓰지 말고 “어떤 subsystem의 어떤 물리적/알고리즘적 의미를 바꾸는가”를 이해해야 튜닝이 가능하다.

22.4 Launch: 시스템 구성을 코드로 만든다#

Launch는 여러 process를 띄우는 편의 기능을 넘어 namespace, remapping, parameter file, 조건, event handler, include 관계를 묶어 재현 가능한 시스템 구성을 만든다. 수동으로 터미널 여러 개를 열어야만 동작하는 시스템은 다른 사람이 재현하기 어렵다.

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(package='learning_ros2', executable='status_pub',
             name='status_pub', parameters=[{'rate_hz': 2.0}]),
        Node(package='learning_ros2', executable='status_sub',
             name='status_sub'),
    ])

Launch Python 해설 Python launch 파일은 일반 Python 스크립트처럼 보이지만 ROS launch 시스템이 generate_launch_description()을 찾아 실행한다. LaunchDescription은 시작할 action들의 묶음이고, launch_ros.actions.Node는 특정 package/executable을 어떤 name·parameter·remap으로 실행할지 선언한다.

22.4.1 대화형 노드를 별도 터미널에서 실행하기#

키보드 입력을 직접 읽는 turtle_teleop_key는 일반 노드와 달리 사용자가 초점을 둔 대화형 터미널이 필요하다. 다음 Launch 파일은 turtlesim_node를 현재 Launch 프로세스에서 시작하고, teleop 노드는 GNOME Terminal 창을 하나 열어 그 안에서 실행한다.

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    turtlesim_node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        name='turtlesim',
        output='screen',
    )

    teleop_node = Node(
        package='turtlesim',
        executable='turtle_teleop_key',
        name='turtle_teleop_key',
        output='screen',
        prefix='gnome-terminal --wait --',
    )

    return LaunchDescription([
        turtlesim_node,
        teleop_node,
    ])

Node의 package와 executable은 실행할 ROS(Robot Operating System, 로봇 운영체제) 패키지와 등록된 실행 파일을 지정하고, name은 ROS Graph에 보이는 노드 이름을 정한다. output='screen'은 표준 출력과 오류를 실행 화면에 보이게 한다. prefix는 실제 노드 명령 앞에 외부 명령을 붙이는 옵션이다. 여기서는 gnome-terminal이 새 창을 열고, --wait는 그 창에서 실행한 명령이 끝날 때까지 터미널 프로세스를 유지하며, 마지막 --는 터미널 옵션과 뒤의 노드 명령을 구분한다.

이 방식은 GNOME Terminal과 그래픽 세션이 있는 Ubuntu 데스크톱에 의존한다. SSH(Secure Shell, 보안 원격 셸) 접속, 컨테이너, CI(Continuous Integration, 지속적 통합) 같은 헤드리스 환경에서는 창을 열 수 없으므로 prefix를 제거하고 turtlesim_node만 Launch로 시작한 뒤, 입력 가능한 별도 터미널에서 ros2 run turtlesim turtle_teleop_key를 실행한다. 터미널 종류가 다르면 prefix 명령도 해당 프로그램의 실행 옵션에 맞게 바꿔야 한다.

Launch 파일은 소스 폴더에 존재하는 것만으로 ros2 launch에서 검색되지 않는다. ament_python 패키지라면 launch 폴더를 package share 디렉터리에 설치하도록 setup.py의 data_files에 등록한다. 아래 예에서는 launch/turtlesim_with_teleop.launch.py를 설치한다.

from glob import glob
import os


data_files=[
    # 기존 항목 유지
    (
        os.path.join('share', package_name, 'launch'),
        glob('launch/*.launch.py'),
    ),
]
cd ~/ros2_ws
colcon build --packages-select turtle_robot --symlink-install
source install/setup.bash
ros2 launch turtle_robot turtlesim_with_teleop.launch.py

setup.py 또는 launch 파일 설치 규칙을 바꾼 뒤에는 다시 빌드하고 새 install/setup.bash를 source해야 한다. ros2 launch의 첫 인자는 패키지 이름이고 둘째 인자는 install 공간의 share/<package>/launch에서 찾을 Launch 파일 이름이다.

22.5 Namespace와 Remapping#

Node 코드는 가능한 한 상대 이름을 사용하고, 배포 시 namespace와 remapping으로 실제 연결을 결정하는 편이 재사용성이 높다. 같은 perception node를 `/robot1`과 `/robot2`에 각각 올릴 수 있는 이유가 이 이름 해석 계층 때문이다.

같은 노드를 여러 로봇에 재사용하려면 하드코딩된 절대 Topic 이름을 줄이고 namespace/remapping을 이용한다. `/robot1/camera/image_raw`와 `/robot2/camera/image_raw`처럼 분리하면 같은 코드로 여러 인스턴스를 운영할 수 있다.

ros2 run turtlesim turtlesim_node --ros-args -r __ns:=/robot1
ros2 run my_pkg vision_node --ros-args -r image:=/camera/color/image_raw

Remapping 해설 -r은 remap 규칙을 뜻한다. __ns:=/robot1은 노드의 namespace를 바꾸는 특별 규칙이고, image:=/camera/color/image_raw처럼 일반 이름도 다른 이름에 연결할 수 있다. 코드를 바꾸지 않고 배포 환경에서 연결만 바꾸는 것이 remapping의 목적이다.

22.6 Lifecycle Node: 시작과 준비 완료를 구분한다#

일반 Node는 프로세스가 뜨면 곧바로 기능을 수행하지만, 실제 시스템에서는 장비 연결·지도 로드·controller 초기화 같은 준비 단계를 분리해야 할 때가 많다. Lifecycle은 configure/activate/deactivate/cleanup을 명시해 살아 있음과 서비스 가능함을 구분한다.

일반 노드는 프로세스가 실행되면 곧바로 동작한다. 하지만 지도 서버나 controller처럼 리소스 준비 순서가 중요한 컴포넌트는 configure와 activate를 분리하는 편이 안전하다. Lifecycle Node는 Unconfigured → Inactive → Active 등의 상태를 명시적으로 관리한다. Nav2가 여러 서버를 안정적으로 bringup하는 데 이 구조를 사용한다.

ros2 lifecycle nodes
ros2 lifecycle get /controller_server
ros2 lifecycle set /some_node configure
ros2 lifecycle set /some_node activate

Lifecycle CLI(Command-Line Interface, 명령줄 인터페이스) 해설 ros2 lifecycle get은 현재 state를 읽고, ros2 lifecycle set은 configure/activate 같은 transition을 요청한다. 프로세스가 실행 중이라는 사실과 lifecycle state가 Active라는 사실은 별개다.

상태와 프로세스의 차이 프로세스가 살아 있다는 사실과 기능이 Active라는 사실은 다르다. Nav2 서버가 node list에 보인다고 바로 navigation 요청을 받을 준비가 된 것은 아니다.

장 핵심 정리#

  • Service는 짧은 request-response, Action은 장시간 작업의 상태 흐름을 표현한다.

  • Parameter는 코드에서 튜닝값을 분리한다.

  • Launch는 시스템 구성을 재현 가능한 코드로 만든다.

  • Lifecycle은 “프로세스 존재”와 “기능 활성화”를 분리한다.

확인 문제#

  1. 그리퍼 닫기 명령을 Service와 Action 중 무엇으로 만들지 판단 기준을 설명하라.

  2. Launch가 단순 shell script보다 좋은 이유를 세 가지 적어라.

  3. Nav2에서 Lifecycle이 유용한 이유를 설명하라.

23장. Topic·Service·Action·Launch·Remapping을 직접 구현하기#

이 장을 끝내면 짧은 RPC와 장시간 작업 인터페이스를 직접 구분하고 구현한다.

23.1 Topic: 생산자와 소비자를 분리한다#

먼저 하나의 정수를 주기적으로 발행하고 다른 노드가 구독해 누적합을 계산한다. 숫자 자체는 단순하지만 Publisher와 Subscriber가 서로의 Python 객체를 전혀 알지 않는다는 점이 핵심이다.

number_pub.py#

import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32


class NumberPublisher(Node):
    def __init__(self):
        super().__init__('number_pub')
        self.declare_parameter('rate_hz', 2.0)
        self.declare_parameter('start', 0)
        rate = float(self.get_parameter('rate_hz').value)
        self.value = int(self.get_parameter('start').value)
        self.pub = self.create_publisher(Int32, 'numbers', 10)
        self.timer = self.create_timer(1.0 / rate, self.tick)

    def tick(self):
        msg = Int32()
        msg.data = self.value
        self.pub.publish(msg)
        self.get_logger().info(f'publish {msg.data}')
        self.value += 1


def main(args=None):
    rclpy.init(args=args)
    node = NumberPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

number_sub.py#

import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32


class NumberSubscriber(Node):
    def __init__(self):
        super().__init__('number_sub')
        self.total = 0
        self.sub = self.create_subscription(Int32, 'numbers', self.on_number, 10)

    def on_number(self, msg):
        self.total += msg.data
        self.get_logger().info(f'received={msg.data}, running_sum={self.total}')


def main(args=None):
    rclpy.init(args=args)
    node = NumberSubscriber()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

23.2 두 터미널에서 실행하고 Graph를 관찰한다#

# 터미널 A
ros2 run r2lab_nodes number_pub

# 터미널 B
ros2 run r2lab_nodes number_sub

# 터미널 C
ros2 topic list -t
ros2 topic info /numbers -v
ros2 topic echo /numbers
ros2 topic hz /numbers
rqt_graph

`topic info -v`에서 endpoint와 QoS(Quality of Service, 서비스 품질) profile까지 본다. “Topic이 존재한다”만으로는 충분하지 않다. Publisher 수, Subscriber 수, 타입, reliability/durability가 기대와 맞는지 확인한다.

23.3 Launch와 Remapping#

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='r2lab_nodes',
            executable='number_pub',
            name='number_source',
            parameters=[{'rate_hz': 4.0, 'start': 10}],
            remappings=[('numbers', '/lab/numbers')],
            output='screen',
        ),
        Node(
            package='r2lab_nodes',
            executable='number_sub',
            name='number_sink',
            remappings=[('numbers', '/lab/numbers')],
            output='screen',
        ),
    ])
ros2 launch r2lab_nodes basics.launch.py
rqt_graph

Launch 파일은 두 프로세스를 단순히 동시에 띄우는 기능을 넘어 node name, parameter, remapping을 한 곳에서 재현한다. 같은 executable을 다른 Topic 구조에 붙일 수 있기 때문에 노드 코드에 절대 Topic 이름을 하드코딩하는 습관을 줄일 수 있다.

23.4 Service: 짧은 요청/응답을 직접 만든다#

`r2lab_interfaces/srv/SetMode.srv`는 요청에 문자열 mode 하나를 받고, 응답으로 accepted와 message를 반환한다.

string mode
---
bool accepted
string message

mode_server.py#

import rclpy
from rclpy.node import Node
from r2lab_interfaces.srv import SetMode


class ModeServer(Node):
    def __init__(self):
        super().__init__('mode_server')
        self.mode = 'idle'
        self.srv = self.create_service(SetMode, 'set_mode', self.on_set_mode)

    def on_set_mode(self, request, response):
        allowed = {'idle', 'manual', 'auto'}
        if request.mode not in allowed:
            response.accepted = False
            response.message = f'unknown mode: {request.mode}'
            return response
        old = self.mode
        self.mode = request.mode
        response.accepted = True
        response.message = f'{old} -> {self.mode}'
        self.get_logger().info(response.message)
        return response


def main(args=None):
    rclpy.init(args=args)
    node = ModeServer()
    try:
        rclpy.spin(node)
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

mode_client.py#

import sys
import rclpy
from rclpy.node import Node
from r2lab_interfaces.srv import SetMode


class ModeClient(Node):
    def __init__(self):
        super().__init__('mode_client')
        self.client = self.create_client(SetMode, 'set_mode')

    def request(self, mode):
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('waiting for /set_mode ...')
        req = SetMode.Request()
        req.mode = mode
        return self.client.call_async(req)


def main(args=None):
    rclpy.init(args=args)
    node = ModeClient()
    mode = sys.argv[1] if len(sys.argv) > 1 else 'auto'
    future = node.request(mode)
    rclpy.spin_until_future_complete(node, future)
    result = future.result()
    if result is not None:
        node.get_logger().info(
            f'accepted={result.accepted}, message={result.message}')
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

비동기 Service 해설 create_client()는 Service client handle을 만든다. wait_for_service()는 server가 discovery될 때까지 기다리고, call_async()는 요청을 보내고 Future를 즉시 돌려준다. Future.result()는 완료된 뒤 실제 Response 객체를 얻는다. callback 안에서 동기 대기를 만들면 executor deadlock으로 이어질 수 있어 비동기 흐름을 기본으로 이해하는 편이 안전하다.

# 서버
ros2 run r2lab_nodes mode_server

# CLI로 직접 요청
ros2 service list -t
ros2 service call /set_mode r2lab_interfaces/srv/SetMode "{mode: auto}"
ros2 service call /set_mode r2lab_interfaces/srv/SetMode "{mode: impossible}"

# Python client
ros2 run r2lab_nodes mode_client auto

Service callback은 가능한 빨리 끝나야 한다. 10초 동안 로봇이 이동하는 동작을 service callback 안에서 직접 수행하면서 다른 callback까지 막는 설계는 피한다. 장시간·취소 가능 작업은 Action의 의미와 맞는다.

23.5 Action: goal-feedback-result-cancel을 직접 만든다#

float32 target_x
float32 duration_sec
---
bool success
string message
---
float32 progress

timed_move_server.py#

import time
import rclpy
from rclpy.action import ActionServer, CancelResponse, GoalResponse
from rclpy.node import Node
from r2lab_interfaces.action import TimedMove


class TimedMoveServer(Node):
    def __init__(self):
        super().__init__('timed_move_server')
        self.server = ActionServer(
            self,
            TimedMove,
            'timed_move',
            execute_callback=self.execute,
            goal_callback=self.on_goal,
            cancel_callback=self.on_cancel,
        )

    def on_goal(self, goal_request):
        if goal_request.duration_sec <= 0.0:
            return GoalResponse.REJECT
        return GoalResponse.ACCEPT

    def on_cancel(self, goal_handle):
        return CancelResponse.ACCEPT

    def execute(self, goal_handle):
        goal = goal_handle.request
        feedback = TimedMove.Feedback()
        steps = 20
        sleep_s = float(goal.duration_sec) / steps

        for i in range(steps):
            if goal_handle.is_cancel_requested:
                goal_handle.canceled()
                result = TimedMove.Result()
                result.success = False
                result.message = 'canceled'
                return result
            time.sleep(sleep_s)
            feedback.progress = float(i + 1) / steps
            goal_handle.publish_feedback(feedback)

        goal_handle.succeed()
        result = TimedMove.Result()
        result.success = True
        result.message = f'reached logical target x={goal.target_x:.2f}'
        return result


def main(args=None):
    rclpy.init(args=args)
    node = TimedMoveServer()
    try:
        rclpy.spin(node)
    finally:
        node.server.destroy()
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Action API(Application Programming Interface, 응용 프로그램 인터페이스) 해설 Action은 Service보다 긴 작업을 위해 goal, feedback, result, cancel 상태를 가진다. GoalResponse는 goal을 받을지 결정하고, CancelResponse는 취소 요청을 허용할지 결정한다. server/client 코드는 goal handle을 통해 각 작업 인스턴스의 상태를 추적한다.

timed_move_client.py#

import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from r2lab_interfaces.action import TimedMove


class TimedMoveClient(Node):
    def __init__(self):
        super().__init__('timed_move_client')
        self.client = ActionClient(self, TimedMove, 'timed_move')

    def send(self, target_x=1.0, duration_sec=4.0):
        self.client.wait_for_server()
        goal = TimedMove.Goal()
        goal.target_x = float(target_x)
        goal.duration_sec = float(duration_sec)
        return self.client.send_goal_async(goal, feedback_callback=self.on_feedback)

    def on_feedback(self, msg):
        self.get_logger().info(f'progress={msg.feedback.progress:.0%}')


def main(args=None):
    rclpy.init(args=args)
    node = TimedMoveClient()
    send_future = node.send()
    rclpy.spin_until_future_complete(node, send_future)
    goal_handle = send_future.result()
    if not goal_handle.accepted:
        node.get_logger().error('goal rejected')
    else:
        result_future = goal_handle.get_result_async()
        rclpy.spin_until_future_complete(node, result_future)
        result = result_future.result().result
        node.get_logger().info(f'{result.success}: {result.message}')
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()
# 터미널 A
ros2 run r2lab_nodes timed_move_server

# 터미널 B: CLI에서 feedback까지 관찰
ros2 action list -t
ros2 action send_goal /timed_move r2lab_interfaces/action/TimedMove \
  "{target_x: 1.0, duration_sec: 5.0}" --feedback

# 터미널 C: Python client
ros2 run r2lab_nodes timed_move_client

Action CLI(Command-Line Interface, 명령줄 인터페이스) 해설 ros2 action send_goal은 CLI에서 Action goal을 보낸다. --feedback을 붙이면 실행 중 feedback 메시지도 계속 표시한다. Action은 goal/result/feedback/cancel 채널을 내부적으로 묶어 하나의 장시간 작업 인터페이스로 제공한다.

이 action은 실제 로봇을 움직이지 않고 시간 경과를 “논리적 이동”으로 모델링한다. 목적은 Action API 자체를 분리해서 배우는 것이다. Nav2 `NavigateToPose`와 MoveIt2 trajectory action을 만날 때도 동일한 goal handle, feedback, result, cancel 사고방식이 반복된다.

23.6 Custom Interface를 바꾸면 왜 재빌드해야 하는가#

`.msg/.srv/.action` 파일은 Python class가 아니라 IDL(Interface Definition Language, 인터페이스 정의 언어) 기반 타입 정의의 입력이다. 빌드 단계에서 언어별 타입 지원 코드가 생성되므로 필드 하나를 바꾸면 interface package를 다시 빌드하고 source해야 한다.

cd ~/ros2_course_ws
colcon build --packages-select r2lab_interfaces r2lab_nodes
source install/setup.bash
ros2 interface show r2lab_interfaces/srv/SetMode
ros2 interface show r2lab_interfaces/action/TimedMove

23.7 의도적으로 고장내기#

  • Publisher Topic을 `/numbers`, Subscriber를 `/number`로 일부러 다르게 만든다. `rqt_graph`와 `topic info`로 찾는다.

  • Publisher는 정상인데 Subscriber의 message type을 다른 타입으로 바꾼다. 타입 계약이 어긋난 결과를 확인한다.

  • Service server를 끈 상태에서 client를 실행해 `wait_for_service()`가 어떤 문제를 해결하는지 본다.

  • Action goal의 `duration_sec`를 음수로 보내 Goal REJECT가 어떻게 나타나는지 확인한다.

Part V. 로봇을 움직이며 배우기#

24장. Jazzy 시뮬레이터와 이동로봇을 ROS Graph로 읽기#

이 장을 끝내면 Gazebo/RViz를 실행한 뒤 로봇의 명령·센서·TF(Transform, 좌표변환) 인터페이스를 Graph로 해부한다.

약어 표기 ROS: Robot Operating System(로봇 운영체제)

24.1 시뮬레이터를 “로봇 그림”이 아니라 인터페이스 제공자로 본다#

Nav2 Jazzy Quickstart의 TurtleBot sandbox를 실행하면 Gazebo, robot_state_publisher, 센서 bridge, odometry, Nav2 서버, RViz 등이 한 번에 올라온다. 처음에는 자율주행 버튼을 누르지 말고 이 시스템이 어떤 Topic과 TF를 제공하는지 해부한다.

source /opt/ros/jazzy/setup.bash
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False

Launch 인자 해설 ros2 launch <package> <launch-file> name:=value 형식이다. headless:=False는 launch argument를 넘기고 launch 파일이 이를 boolean으로 해석한다. ros2 launch ... --show-args를 사용하면 해당 launch 파일이 노출하는 인자를 먼저 확인할 수 있다.

24.2 반드시 확인할 다섯 가지#

ros2 node list
ros2 topic list -t
ros2 action list -t
ros2 lifecycle nodes
ros2 run tf2_tools view_frames
확인 대상 질문 대표 확인
/scan LiDAR가 어느 frame과 QoS로 나오는가? topic type/info/echo
/odom local odometry가 실제로 갱신되는가? topic echo --once
/tf map→odom→base_link→sensor chain이 이어지는가? tf2_echo/view_frames
velocity command 현재 simulator가 어떤 메시지 타입을 받는가? topic list -t / info
Nav2 actions 목표 이동 인터페이스 이름/타입은 무엇인가? action list -t / action info

현재 Nav2 minimal TurtleBot simulation에서는 속도 bridge가 `geometry_msgs/msg/TwistStamped`를 사용하는 구성이 있다. 다른 로봇이나 기존 TurtleBot 패키지는 `Twist`일 수 있다. 따라서 교재 예제를 맹목적으로 실행하지 말고 항상 `ros2 topic list -t`로 실제 타입을 먼저 확인한다.

24.3 Nav2를 쓰지 않고 로봇 명령 경로만 확인#

Nav2가 사용하는 velocity command topic 이름은 구성에 따라 달라질 수 있다. `ros2 topic list -t | grep cmd_vel`과 `ros2 topic info`로 실제 최종 command topic을 확인한 뒤 한 번만 작은 명령을 보낸다.

pipe·grep 해설 grep cmd_vel은 표준입력에서 cmd_vel이 들어간 줄만 남긴다. ros2 topic list -t | grep cmd_vel은 많은 Topic 중 velocity command 후보를 빠르게 찾는 조합이다. 이름만 보고 타입을 가정하지 말고 이어서 ros2 topic info <name> -v로 실제 타입과 publisher를 확인한다.

ros2 topic list -t | grep cmd_vel
# 예: 실제 타입이 TwistStamped이고 topic이 /cmd_vel일 때
ros2 topic pub --once /cmd_vel geometry_msgs/msg/TwistStamped \
  "{twist: {linear: {x: 0.05}, angular: {z: 0.0}}}"

안전한 실습 습관 실제 로봇에서는 command Topic을 확인한 뒤에도 바로 값을 보내지 않는다. 비상정지, 속도 제한, 주변 공간, controller state를 먼저 확인한다. 이 장의 직접 publish는 시뮬레이터에서만 수행한다.

24.4 센서 message를 읽는다#

ros2 interface show sensor_msgs/msg/LaserScan
ros2 topic echo /scan --once
ros2 topic hz /scan
ros2 topic info /scan -v

LaserScan의 `ranges[i]`만 보지 말고 `angle_min`, `angle_increment`, `range_min/max`, `header.frame_id`, timestamp를 같이 본다. 25장의 회피 노드는 이 metadata로 “정면 ±몇 도”의 index를 계산한다.

24.5 TF를 숫자로 확인한다#

TF는 Transform(좌표 변환)의 약자다. ROS 2(Robot Operating System 2, 로봇 운영체제 2)의 TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)는 로봇 각 부분의 좌표계(Frame) 사이에서 위치와 방향의 관계를 찾아 주는 좌표 변환 시스템이다. TF2는 TF 라이브러리의 2세대 구현이며 ROS 2에서는 tf2_ros와 /tf, /tf_static을 사용한다. 변환을 조회하기 전에 각 좌표계 이름부터 확인한다.

ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link <laser_frame>

첫 변환은 로봇이 움직일 때 계속 변해야 하고, 센서가 base에 고정되어 있다면 두 번째 변환은 일정해야 한다. 이 관찰만으로 odometry와 sensor extrinsic의 역할을 분리할 수 있다.

25장. 실습 프로젝트 — LaserScan + FSM 장애물 회피#

이 장을 끝내면 LaserScan을 읽어 장애물을 피하는 상태기계를 완성하고 parameter로 동작을 바꾼다.

약어 표기 FSM: Finite State Machine(유한 상태 기계)

25.1 완성할 행동#

FSM(Finite State Machine, 유한 상태 기계)은 시스템이 유한한 개수의 상태(State) 가운데 한 상태에 머물다가 입력 또는 조건에 따라 정해진 규칙(Transition)으로 다른 상태로 바뀌는 동작 모델이다. 여기서 ‘유한’은 동작 상태의 종류가 미리 정해져 있다는 뜻이다. State는 현재 동작 모드, Transition은 상태를 바꾸는 조건과 규칙을 가리킨다.

이번 실습의 상태는 FORWARD(전진)와 TURN(회전) 두 가지다. 초기 상태는 FORWARD이며, LaserScan으로 계산한 전방 최단 거리가 stop_distance(기본 0.55 m)보다 작아지면 TURN으로 전환한다. 회전 중 전방 거리가 clear_distance(기본 0.75 m)보다 커지면 FORWARD로 복귀한다. 두 기준 사이에서는 현재 상태를 유지한다. 이렇게 진입·복귀 임계값을 다르게 두는 기법을 히스테리시스(hysteresis)라 하며, 센서 측정값이 경계에서 조금 흔들릴 때 상태가 빠르게 왕복하는 현상(state chatter)을 줄인다.

FORWARD (전진) TURN (회전) 전방 거리 < 0.55 m 전방 거리 > 0.75 m
그림 25-1. 장애물 회피 FSM의 상태와 전이 조건. 서로 다른 두 거리 임계값 사이에서는 현재 상태를 유지한다.

이 동작을 ROS 2(Robot Operating System 2, 로봇 운영체제 2)로 구현하려면 /scan Topic을 읽어 전방 거리를 계산하는 callback, FORWARD 또는 TURN 상태를 저장하는 변수, 주기적으로 상태 전이를 판단하는 제어 함수, /cmd_vel Topic으로 속도를 보내는 Publisher가 필요하다. QoS(Quality of Service, 서비스 품질)는 센서 데이터 수신 방식을, Parameter는 거리와 속도 기준을 조절한다. 다음 절에서 이들을 하나의 Node에 연결한다.

25.2 먼저 계산 그래프를 종이에 그린다#

/scan (LaserScan) ---> bump_go ---> /cmd_vel (TwistStamped)
                         |
                         +-- state: FORWARD / TURN
                         +-- params: stop_distance, speeds, front_angle

그림의 FSM 상태는 ROS 2의 Node와 다른 개념이다. 이 예제에서 ROS(Robot Operating System, 로봇 운영체제) Node는 bump_go 하나이며, 그 안의 self.state가 FORWARD 또는 TURN 값을 가진다. 따라서 Node 간 통신 구조와 Node 내부의 상태 전이 규칙을 구분하여 읽어야 한다.

25.3 전체 구현#

import math
from enum import Enum

import rclpy
from geometry_msgs.msg import TwistStamped
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan


class State(Enum):
    FORWARD = 1
    TURN = 2


class BumpGo(Node):
    def __init__(self):
        super().__init__('bump_go')
        self.declare_parameter('scan_topic', '/scan')
        self.declare_parameter('cmd_vel_topic', '/cmd_vel')
        self.declare_parameter('stop_distance', 0.55)
        self.declare_parameter('clear_distance', 0.75)
        self.declare_parameter('linear_speed', 0.15)
        self.declare_parameter('angular_speed', 0.7)
        self.declare_parameter('front_half_angle_deg', 20.0)

        self.stop_distance = float(self.get_parameter('stop_distance').value)
        self.clear_distance = float(self.get_parameter('clear_distance').value)
        self.linear_speed = float(self.get_parameter('linear_speed').value)
        self.angular_speed = float(self.get_parameter('angular_speed').value)
        self.front_half_angle = math.radians(
            float(self.get_parameter('front_half_angle_deg').value))

        scan_topic = str(self.get_parameter('scan_topic').value)
        cmd_topic = str(self.get_parameter('cmd_vel_topic').value)

        self.state = State.FORWARD
        self.front_distance = math.inf
        self.scan_sub = self.create_subscription(
            LaserScan, scan_topic, self.on_scan, qos_profile_sensor_data)
        self.cmd_pub = self.create_publisher(TwistStamped, cmd_topic, 10)
        self.timer = self.create_timer(0.1, self.control_step)

        self.get_logger().info(
            f'BumpGo ready: scan={scan_topic}, cmd={cmd_topic}, '
            f'stop={self.stop_distance:.2f}m')

    def on_scan(self, scan: LaserScan):
        values = []
        for i, distance in enumerate(scan.ranges):
            angle = scan.angle_min + i * scan.angle_increment
            if abs(angle) <= self.front_half_angle:
                if math.isfinite(distance) and scan.range_min < distance < scan.range_max:
                    values.append(distance)
        self.front_distance = min(values) if values else math.inf

    def control_step(self):
        if self.state == State.FORWARD and self.front_distance < self.stop_distance:
            self.state = State.TURN
            self.get_logger().info('FORWARD -> TURN')
        elif self.state == State.TURN and self.front_distance > self.clear_distance:
            self.state = State.FORWARD
            self.get_logger().info('TURN -> FORWARD')

        cmd = TwistStamped()
        cmd.header.stamp = self.get_clock().now().to_msg()
        cmd.header.frame_id = 'base_link'
        if self.state == State.FORWARD:
            cmd.twist.linear.x = self.linear_speed
        else:
            cmd.twist.angular.z = self.angular_speed
        self.cmd_pub.publish(cmd)


def main(args=None):
    rclpy.init(args=args)
    node = BumpGo()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        stop = TwistStamped()
        stop.header.stamp = node.get_clock().now().to_msg()
        node.cmd_pub.publish(stop)
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Python 보조 개념 Enum은 가능한 상태 값을 문자열로 흩뿌리지 않고 제한된 집합으로 정의하는 표준 라이브러리 타입이다. FSM에서 FORWARD, TURN 같은 상태를 Enum으로 두면 오타와 임의 값 사용을 줄일 수 있다. qos_profile_sensor_data는 고빈도 센서 스트림에 맞춘 ROS 2의 미리 정의된 QoS profile이다.

코드 읽는 순서: State(Enum)가 가능한 상태를 제한하고, on_scan()은 /scan에서 전방 거리만 갱신한다. control_step()은 0.1초마다 현재 상태와 거리 임계값을 비교해 상태를 바꾼 후 TwistStamped 명령을 발행한다. 즉 센서 갱신과 상태 전이·출력을 역할별로 분리했다.

25.4 실행#

# 터미널 A: simulator
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False

# 터미널 B: 교재 workspace
source ~/ros2_course_ws/install/setup.bash
ros2 run r2lab_nodes bump_go --ros-args \
  --params-file ~/ros2_course_ws/src/r2lab_nodes/config/bump_go.yaml

Nav2가 동시에 velocity command를 보내면 두 명령원이 경쟁할 수 있다. FSM 실습에서는 Nav2 goal을 주지 않고, 필요하면 Nav2 controller를 비활성화하거나 command mux 구조를 명확히 한다. 실제 로봇에서 “여러 노드가 같은 actuator topic을 publish”하는 상황은 매우 중요한 설계 문제다.

25.5 정상이라면 무엇을 보아야 하는가#

  • `ros2 node info /bump_go`에서 `/scan` subscriber와 command publisher가 보인다.

  • 장애물에 가까워지면 로그에서 FORWARD → TURN 전이를 확인하고, 전방이 다시 충분히 비면 TURN → FORWARD 전이를 확인한다.

  • RViz/Gazebo에서 로봇이 벽을 향해 무한 직진하지 않고 방향을 바꾼다.

  • `ros2 topic hz /scan`은 센서 publish rate를 보여주고, `topic info -v`는 sensor QoS를 보여준다.

25.6 파라미터를 물리량으로 해석한다#

`stop_distance`는 단순 숫자가 아니라 로봇 크기, 속도, controller 지연, 센서 노이즈를 포함한 정지/회피 여유거리다. `front_half_angle_deg`가 너무 작으면 얇은 전방 ray만 보고 장애물을 놓치고, 너무 크면 옆 벽 때문에 계속 회전할 수 있다. Parameter tuning은 코드 미세조정이 아니라 시스템의 물리 의미를 조절하는 작업이다.

25.7 의도적으로 고장내기#

  • scan Topic 이름을 틀리게 remap한다. Node는 살아 있지만 sensor callback이 한 번도 호출되지 않는 상황을 `node info`로 찾는다.

  • Subscriber QoS를 기본 RELIABLE로 바꾸고 simulator sensor endpoint와 호환되는지 확인한다. 데이터가 사라진다면 `topic info -v`로 QoS를 비교한다.

  • `stop_distance`를 지나치게 작게 해 충돌 가능성이 커지는 것을 시뮬레이터에서 관찰한다.

  • stop_distance와 clear_distance를 일부러 같은 값으로 설정해 센서값이 경계에서 흔들릴 때 상태가 자주 왕복하는지 관찰한다. 그런 다음 기본값(0.55 m와 0.75 m)으로 복원하여 히스테리시스가 전이 빈도를 낮추는지 비교한다.

Part VI. 공간·로봇 모델·상태추정#

26장. TF2와 SE(2)/SE(3): 좌표와 시간을 함께 다루기#

이 장을 끝내면 frame과 timestamp를 사용해 센서 좌표를 다른 좌표계로 변환하고 오류를 진단한다.

약어 표기 TF2: ROS 2 Transform Library(ROS 2 좌표변환 라이브러리)

26.1 좌표는 frame 없이 의미가 없다#

Frame(기준 좌표계)은 원점과 축 방향을 정한 좌표 표현의 기준이다. Transform(TF)은 서로 다른 frame에 표현된 같은 점·자세를 대응시키는 회전과 평행이동 관계다. 예를 들어 map은 지도 기준, odom은 연속 주행 추정 기준, base_link는 로봇 본체 기준, laser는 센서 기준이다. 이 frame들은 TF tree에서 부모–자식 관계로 연결된다.

로봇 좌표의 최소 단위는 `(x,y,z)`가 아니라 값·frame·time이다. 같은 `(1,0,0)`이라도 camera frame과 base frame에서는 전혀 다른 위치다. TF2를 배우는 목적은 행렬 암기가 아니라 센서·로봇·지도 좌표를 하나의 일관된 관계망으로 다루는 데 있다.

카메라가 물체를 `(0.25, -0.10, 0.80)`에서 검출했다는 말만으로는 로봇팔을 움직일 수 없다. 이 값이 `camera_optical_frame` 기준인지 `base_link` 기준인지에 따라 같은 숫자가 전혀 다른 공간을 가리킨다. 좌표를 전달할 때 frame_id가 중요한 이유다.

그림 26-1. 하나의 좌표는 TF tree를 따라 다른 frame 표현으로 변환된다.

26.2 SE(2): 평면 로봇의 pose#

SE는 Special Euclidean의 약자다. 정확히는 SE(2)가 2차원 특수 유클리드 군(Special Euclidean Group)이다. 거리와 각도를 보존하는 평면 강체변환, 즉 회전과 평행이동을 하나로 다룬다. 여기서 군(Group)이란 변환을 차례로 합성해도 같은 종류의 변환이 되고, 항등변환과 역변환도 존재한다는 뜻이다. 따라서 한 프레임에서 다음 프레임으로 이동하는 변환들을 연쇄적으로 연결할 수 있다.

평면 이동로봇의 pose는 일반적으로 `(x, y, θ)`로 표현한다. 이를 homogeneous transform으로 쓰면 회전과 평행이동을 하나의 행렬곱으로 합칠 수 있다.

T=[cosθ−sinθxsinθcosθy001] T=\begin{bmatrix}\cos\theta&-\sin\theta&x\\ \sin\theta&\cos\theta&y\\ 0&0&1\end{bmatrix}
where
T평면 좌표계 사이의 동차변환행렬, 3×3
x, y기준 좌표계에서의 평행이동 위치 [m]
θ평면에서의 회전각(yaw) [rad]

어떤 점 `p_B`가 B frame 좌표로 주어졌을 때 `T_A_B`를 곱하면 A frame의 좌표 `p_A`로 바뀐다. 표기에서 가장 중요한 것은 행렬 자체보다 방향을 일관되게 읽는 것이다: `T_A_B`는 “B로 표현된 좌표를 A 표현으로 바꾸는 변환”이라고 읽는다.

26.3 SE(3): 3차원 강체변환#

SE(3)는 3차원 특수 유클리드 군(Special Euclidean Group in 3D)으로, 공간에서 길이와 각도를 바꾸지 않는 회전·평행이동의 집합이다. 이에 비해 SO(3)는 Special Orthogonal Group in 3D(3차원 특수 직교군)로, 평행이동을 제외한 회전만을 나타낸다. SE(3)의 회전 부분 R은 SO(3)에 속하며 RᵀR = I, det(R) = +1을 만족한다. SE(3)의 평행이동 부분 t를 합쳐 하나의 4×4 동차변환행렬을 만든다.

3D 로봇팔과 카메라는 SE(3)가 기본이다. 회전행렬 R과 평행이동 t를 4×4 행렬로 묶으면 `T_base_camera @ T_camera_object`처럼 체인을 계산할 수 있다. Hand-Eye와 MoveIt2에서 반복해서 쓰게 되는 핵심 표기다.

ATB=[ARBA𝐭B𝟎T1],𝐩A=ATB𝐩B {}^{A}\!T_{B}=\begin{bmatrix}{}^{A}R_{B}&{}^{A}\mathbf t_{B}\\\mathbf0^{T}&1\end{bmatrix},\qquad \mathbf p_{A}={}^AT_{B}\,\mathbf p_{B}
where
A, B목표 좌표계와 원본 좌표계
R3×3 회전행렬, SO(3)의 원소
t3×1 평행이동 벡터 [m]
p동차좌표(마지막 성분이 1인 점 좌표)

`R`은 3×3 회전행렬, `t`는 3×1 이동벡터다. 여러 frame 변환은 행렬곱으로 연결된다. 예를 들어 `T_base_object = T_base_camera · T_camera_object`다. 이것이 비전 좌표를 로봇 base 좌표로 바꾸는 핵심이다.

26.4 역변환#

역변환은 화살표를 반대로 읽는 것이다. translation에 단순히 마이너스를 붙이는 것이 아니라 회전된 좌표계에서 다시 표현해야 하므로 `-R^T t`가 된다. TF chain을 손으로 검산할 때 이 공식을 알면 방향 오류를 빠르게 찾을 수 있다.

회전행렬은 직교행렬이므로 역행렬이 transpose다. 그러나 전체 강체변환의 역변환은 translation까지 함께 바뀐다.

BTA=(ATB)−1,BRA=(ARB)T,B𝐭A=−(ARB)TA𝐭B {}^{B}T_{A}=({}^{A}T_{B})^{-1},\quad{}^{B}R_{A}=({}^{A}R_{B})^{T},\quad{}^{B}\mathbf t_{A}=-({}^{A}R_{B})^{T}{}^{A}\mathbf t_{B}
where
A, B두 기준 좌표계
T⁻¹A에서 B로의 좌표변환을 되돌리는 역변환
Rᵀ회전행렬 R의 전치; 직교회전이므로 역행렬과 동일
t평행이동 벡터 [m]

단순히 `t`에 마이너스만 붙이면 틀린 이유는 translation 벡터 자체도 다른 frame으로 회전되어야 하기 때문이다.

26.5 Euler angle과 Quaternion#

Euler angle은 사람이 방향각을 읽기 쉽지만 회전 순서에 따라 표현이 달라지고 특이점(gimbal lock)이 생길 수 있다. Quaternion(사원수)은 `(x,y,z,w)` 네 성분으로 3D 회전을 표현하며 보간과 회전 합성에 적합하다. ROS(Robot Operating System, 로봇 운영체제) 메시지의 orientation에서 사용하는 단위 quaternion은 `x²+y²+z²+w²=1`을 만족한다. 각 성분을 roll/pitch/yaw로 직접 읽지 말고 변환 함수를 사용하며 정규화 여부를 검증한다.

실무 실수 geometry_msgs/Pose의 position.x/y/z와 orientation.x/y/z/w는 이름만 비슷할 뿐 전혀 다른 물리량이다. Quaternion의 x/y/z를 roll/pitch/yaw처럼 해석하지 않는다.

26.6 TF2는 transform database다#

수학의 SE(3)와 ROS 2(Robot Operating System 2, 로봇 운영체제 2)의 TF2는 같은 말이 아니다. SE(3)는 좌표 변환을 합성·역변환하는 수학적 구조이고, TF2는 각 노드가 발행한 변환을 시간 정보와 함께 수집하여 필요한 frame 사이의 변환을 조회하는 소프트웨어다. `tf2_echo odom base_link`는 base_link에서 표현한 데이터를 odom 기준으로 옮길 변환을 보여 준다. 로봇팔의 3차원 변환은 SE(3), TurtleBot3의 평면 위치 추정은 흔히 SE(2)로 모델링하지만 TF2는 둘 모두의 실제 frame 관계를 다룬다.

TF2의 핵심은 현재 transform 하나가 아니라 시간에 따른 transform buffer다. 센서 메시지의 stamp 시점에서 source→target 관계를 조회하므로 로봇이 움직이는 중에도 올바른 좌표 변환을 할 수 있다. 그래서 frame tree와 timestamp를 함께 봐야 한다.

TF2는 frame 사이의 transform을 시간축과 함께 buffer에 저장하고, 사용자가 특정 시각의 source→target 변환을 요청하면 tree를 따라 필요한 chain을 계산한다. 따라서 TF2는 단순한 행렬 라이브러리가 아니라 분산된 여러 노드가 제공하는 좌표관계를 통합하는 시스템이다.

ros2 run tf2_ros tf2_echo base_link camera_link
ros2 run tf2_tools view_frames

TF CLI(Command-Line Interface, 명령줄 인터페이스) 해설 tf2_echo target source는 source 좌표를 target에서 표현하기 위한 transform을 계속 출력한다. 인자 순서를 반대로 바꾸면 역변환이 나오므로 frame 방향을 확인하는 데 유용하다. view_frames는 현재 TF tree를 파일로 시각화한다.

26.7 Static transform과 Dynamic transform#

로봇 본체에 고정된 카메라와 `base_link` 사이 관계는 static transform으로 `/tf_static`에 발행한다. 반면 바퀴 주행의 `odom→base_link`와 관절 움직임에 따른 Link 관계는 시간에 따라 바뀌므로 `/tf`에 지속적으로 발행한다. 동일한 parent–child transform을 서로 다른 노드가 중복 발행하지 않도록 발행 책임을 명확히 구분한다.

26.8 TF2의 시간과 extrapolation#

센서 메시지의 timestamp가 10.0초인데 TF buffer에 그 시각의 변환이 없으면 extrapolation 오류가 발생한다. 버퍼 범위를 벗어난 과거 데이터는 past, 아직 발행되지 않은 미래 시각은 future extrapolation 문제로 나타날 수 있다. 이때 최신 변환을 무조건 사용하면 오차를 숨길 뿐이므로 센서 `header.stamp`, transform 발행 주기, `/clock`과 `use_sim_time`, 네트워크 지연 및 buffer 범위를 함께 확인한다.

26.9 Python에서 Point/Pose 변환#

실전에서는 좌표 3개만 넘기지 말고 `PointStamped` 또는 `PoseStamped`를 사용한다. Header가 source frame과 timestamp를 보존하기 때문에 transform 결과의 의미를 추적할 수 있고 rosbag으로 재생할 때도 당시 시간축을 유지할 수 있다.

# 개념 예시: Buffer에서 target<-source transform을 구해 Pose를 변환
from rclpy.time import Time

transform = tf_buffer.lookup_transform(
    'base_link',                 # target
    msg.header.frame_id,         # source
    msg.header.stamp)
# 이후 tf2_geometry_msgs helper 등으로 PoseStamped 변환

TF2 API(Application Programming Interface, 응용 프로그램 인터페이스) 해설 Buffer는 시간에 따른 transform을 저장하는 TF2 데이터베이스다. lookup_transform(target, source, time)은 source frame 데이터를 target frame으로 표현하기 위해 필요한 변환을 요청한다. 메시지의 timestamp를 넘기는 이유는 로봇이 움직이는 동안 좌표계 관계도 시간에 따라 달라질 수 있기 때문이다.

26.10 Hand-Eye Calibration을 TF 관점에서 이해하기#

Hand-Eye Calibration은 결국 TF tree에 들어갈 고정 transform을 추정하는 문제다. Eye-in-hand는 tool↔︎camera, Eye-to-hand는 base/world↔︎camera 관계가 핵심이다. 보정 결과 숫자보다 어떤 frame에서 어떤 frame으로 가는 transform인가를 명확히 기록해야 한다.

Hand-Eye Calibration의 목표는 카메라와 로봇의 고정된 상대변환을 추정하는 것이다. Eye-in-hand에서는 카메라가 tool과 함께 움직이므로 `tool→camera`가 고정이고, Eye-to-hand에서는 카메라가 외부에 고정되어 `base/world→camera` 관계를 추정한다. 여러 pose 쌍에서 얻은 운동 관계는 고전적으로 `AX = XB` 형태의 문제로 정리된다.

장 핵심 정리#

  • 좌표는 frame과 시간 없이는 불완전하다.

  • SE(2)는 평면, SE(3)는 공간의 강체변환이며, SO(3)는 3차원 회전이다. 동차변환은 회전과 이동을 하나의 연산으로 연결한다.

  • TF2는 transform chain을 시간과 함께 관리한다.

  • Hand-Eye 결과는 TF tree에 들어가는 고정 transform으로 생각하면 이해가 쉽다.

확인 문제#

  1. `T_base_camera`와 `T_camera_object`가 있을 때 `T_base_object`를 식으로 쓰라.

  2. TF extrapolation 오류가 발생하는 원인을 세 가지 설명하라.

  3. Eye-in-hand와 Eye-to-hand에서 어떤 transform이 고정인지 설명하라.

27장. URDF·Xacro·robot_state_publisher·RViz2#

이 장을 끝내면 link/joint 모델을 읽고 robot_state_publisher와 RViz의 관계를 설명한다.

약어 표기 URDF: Unified Robot Description Format(통합 로봇 기술 형식) · Xacro: XML Macros(XML 매크로) · RViz2: ROS Visualization 2(ROS 시각화 도구)

27.1 URDF가 표현하는 것과 표현하지 않는 것#

URDF(Unified Robot Description Format)는 Link(강체)와 Joint(두 Link 사이 운동 관계)를 연결하여 로봇의 운동학적 구조를 기술하는 XML(Extensible Markup Language, 확장 마크업 언어) 형식이다. 각 Link의 `visual`은 화면 표시 형상, `collision`은 충돌 판정 형상, `inertial`은 질량·관성 등의 동역학 특성을 담당한다. 목적과 계산량이 다르므로 세 요소에 동일한 정밀 mesh를 그대로 사용하기보다 용도에 맞는 형상과 물성을 정의한다.

<link name="base_link">
  <visual>
    <geometry><box size="0.30 0.30 0.10"/></geometry>
  </visual>
</link>
<joint name="arm_joint" type="revolute">
  <parent link="base_link"/>
  <child link="arm_link"/>
  <origin xyz="0 0 0.30" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit lower="-1.57" upper="1.57" effort="10" velocity="1"/>
</joint>

URDF/XML 해설 URDF는 XML 문서다. <link>는 강체, <joint>는 두 link 사이의 운동관계다. <parent>와 <child>가 kinematic tree 방향을 만들고, <origin xyz="..." rpy="...">는 부모 기준 위치와 roll/pitch/yaw 오프셋을 지정한다.

27.2 Joint 종류와 kinematic tree#

Joint는 두 link 사이 자유도를 정의한다. `revolute`는 제한 회전, `continuous`는 무제한 회전, `prismatic`은 직선, `fixed`는 고정이다. 잘못된 axis/origin은 RViz에서는 그럴듯해 보여도 실제 TF와 planning 결과를 틀리게 만든다.

Joint 특징 예
fixed 상대 pose 고정 센서 마운트
revolute 제한 범위 회전 일반 로봇팔 관절
continuous 무제한 회전 바퀴
prismatic 직선 이동 리니어 액추에이터
floating/planar 특수 자유도 특정 모델링 상황

URDF는 tree 구조를 전제로 한다. closed kinematic chain을 직접 표현하는 데 제한이 있다는 점도 알아두면 좋다.

27.3 Xacro: 중복을 코드화한다#

바퀴의 좌우 대칭 구조, 센서 마운트, 길이와 질량처럼 URDF에서 반복되는 요소는 Xacro 변수·매크로·조건으로 묶을 수 있다. Xacro는 로봇을 직접 구동하는 실행 형식이 아니라 최종 URDF XML을 생성하는 전처리 도구다. 하나의 소스에서 여러 로봇 변형(variant)을 생성해도 최종적으로는 Link·Joint 관계가 유효한 URDF인지 확인해야 한다.

<xacro:property name="wheel_radius" value="0.05"/>
<xacro:macro name="wheel" params="name y"> ... </xacro:macro>

Xacro 해설 Xacro는 URDF XML에 변수와 macro 기능을 더한 전처리 도구다. xacro:property는 재사용할 값을 정의하고, xacro:macro는 반복되는 XML 조각을 함수처럼 만든다. 실행 시 Xacro가 일반 URDF XML로 펼쳐진 뒤 robot_state_publisher 등이 사용한다.

27.4 robot_state_publisher#

robot_state_publisher는 URDF의 고정 기하와 현재 `/joint_states`를 결합해 link 간 TF를 계산한다. RobotModel이 멈춰 있다면 URDF만 볼 것이 아니라 joint state가 실제로 들어오는지, joint 이름이 URDF와 일치하는지 확인한다.

URDF만 있다고 현재 link pose가 자동으로 생기지 않는다. `robot_state_publisher`는 `robot_description`에 저장된 모델과 `/joint_states`의 현재 joint angle을 결합해 각 link의 TF를 계산해 `/tf`로 발행한다. fixed joint는 `/tf_static`으로 나갈 수 있다.

/joint_states + robot_description -> robot_state_publisher -> /tf, /tf_static

27.5 joint_state_publisher와 실제 robot driver#

학습용 joint_state_publisher는 실제 encoder가 없는 상황에서 가상 joint 값을 만들어 주는 도구다. 실제 로봇 driver와 동시에 같은 joint state를 발행하면 서로 다른 상태원이 충돌할 수 있으므로 하드웨어 연결 시에는 제거한다.

학습 중에는 `joint_state_publisher_gui`로 slider를 움직여 joint state를 가상으로 만들 수 있다. 실제 E0509에서는 robot driver/controller가 실제 joint position을 제공한다. 둘을 동시에 실행해 같은 joint를 발행하면 충돌하거나 엉뚱한 상태가 보일 수 있다.

27.6 RViz2는 시뮬레이터가 아니다#

RViz2는 ROS 2(Robot Operating System 2, 로봇 운영체제 2) 데이터의 시각화·진단 도구로서 RobotModel, TF(Transform, 좌표변환), LaserScan, PointCloud2, Image, Path, Marker, Costmap 등을 표시한다. Gazebo는 물리 world에서 동역학과 센서 모델을 계산한다. RViz에서 로봇이 벽을 통과하는 것처럼 보여도 RViz 자체가 충돌을 계산하는 것은 아니므로, TF·표시 문제와 Gazebo의 물리·센서 문제를 구분해야 한다. MoveIt2의 충돌 판정과 ros2_control의 구동도 각각 별도 계층이다.

27.7 Fixed Frame의 의미#

RViz2의 Fixed Frame은 표시할 모든 데이터를 최종적으로 변환해 놓을 기준 좌표계다. 지도 기반 Nav2에서는 흔히 `map`, 단독 로봇팔에서는 `base_link`를 선택한다. LaserScan이나 RobotModel이 사라지거나 No transform 오류가 발생하면, 먼저 메시지의 `frame_id`에서 Fixed Frame까지 TF chain이 이어지는지와 필요한 timestamp의 변환이 존재하는지 확인한다.

27.8 URDF 실습: 1축에서 3축으로 확장#

앞 절에서 확인한 link, joint, parent/child, origin, axis를 작은 로봇팔에 적용한다. 관절을 하나씩 추가하고 check_urdf로 링크 트리를 확인한 뒤, joint_state_publisher_gui의 값이 robot_state_publisher의 TF로 이어지는지 RViz2에서 검증한다.

워크스페이스의 src 디렉터리에서 Python 패키지를 만든다.

Bash
cd ~/ros2_ws/src
ros2 pkg create simple_bot_description --build-type ament_python
mkdir -p simple_bot_description/urdf simple_bot_description/launch

simple_bot_description/urdf/simple_bot.urdf에 world와 base_link를 fixed joint로 연결하고 link1은 0.30 m, link2는 0.40 m로 둔다. 각 visual 원통의 중심은 링크 길이의 절반에 놓는다. 1축 단계의 joint1은 base_link에서 link1을 연결하며 origin z=0.05 m, 회전축 Z를 사용한다.

XML
<joint name="joint1" type="revolute">
  <parent link="base_link"/>
  <child link="link1"/>
  <origin xyz="0 0 0.05" rpy="0 0 0"/>
  <axis xyz="0 0 1"/>
  <limit lower="-3.14" upper="3.14" effort="10" velocity="1"/>
</joint>

2축은 link1 끝에서 link2를 연결한다. origin z는 0.30 m이고 Y축 회전으로 link2가 세로 평면에서 기울도록 한다.

XML
<joint name="joint2" type="revolute">
  <parent link="link1"/>
  <child link="link2"/>
  <origin xyz="0 0 0.30" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit lower="-1.57" upper="1.57" effort="10" velocity="1"/>
</joint>

3축은 link2 끝에서 link3를 연결한다. origin z는 0.40 m다. joint3도 Y축으로 지정하면 joint2와 같은 평면에서 팔꿈치가 굽혀진다. 축을 바꾸면 링크가 움직이는 평면도 달라진다.

XML
<joint name="joint3" type="revolute">
  <parent link="link2"/>
  <child link="link3"/>
  <origin xyz="0 0 0.40" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit lower="-1.57" upper="1.57" effort="10" velocity="1"/>
</joint>

각 단계에서 링크 이름과 트리를 검사한다. check_urdf는 XML 문법과 연결 트리를 확인하지만 관절의 시각적 배치까지 판단하지는 않는다.

Text
check_urdf ~/ros2_ws/src/simple_bot_description/urdf/simple_bot.urdf

예상 트리는 world → base_link → link1이며, link2와 link3가 차례로 이어진다. setup.py의 data_files에 URDF와 launch 파일을 등록하고 package.xml에 ament_index_python, launch, launch_ros, robot_state_publisher, joint_state_publisher_gui, rviz2 실행 의존성을 선언한다.

Python
from glob import glob

(os.path.join("share", package_name, "urdf"), glob("urdf/*.urdf")),
(os.path.join("share", package_name, "launch"), glob("launch/*.launch.py")),

launch 파일은 설치된 URDF를 robot_description에 전달하고 노드를 시작한다. joint_state_publisher_gui는 학습용 관절값을 만들며 실제 모터를 구동하지 않는다.

Python
from pathlib import Path
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    package_dir = Path(get_package_share_directory("simple_bot_description"))
    robot_description = (package_dir / "urdf" / "simple_bot.urdf").read_text(encoding="utf-8")
    return LaunchDescription([
        Node(package="robot_state_publisher", executable="robot_state_publisher",
             parameters=[{"robot_description": robot_description}], output="screen"),
        Node(package="joint_state_publisher_gui", executable="joint_state_publisher_gui", output="screen"),
        Node(package="rviz2", executable="rviz2", arguments=["-f", "world"], output="screen"),
    ])

빌드 후 launch를 실행한다. RViz2의 Fixed Frame을 world로 설정하고 RobotModel과 TF display를 추가해 관절 슬라이더를 움직인다.

Bash
cd ~/ros2_ws
colcon build --symlink-install --packages-select simple_bot_description
source install/setup.bash
ros2 launch simple_bot_description display_rviz.launch.py

robot_description은 URDF 파일 경로가 아니라 파일에서 읽은 XML 문자열이다. launch가 그 문자열을 robot_state_publisher의 parameter로 전달한다. Parameter는 Node의 설정값이고 Topic은 Node 사이의 데이터 통로다. Jazzy의 robot_state_publisher는 모델을 /robot_description Topic에도 transient local QoS(Quality of Service, 서비스 품질)로 발행한다.

다른 터미널에서 parameter와 Topic의 실제 값을 확인한다. transient local 설정은 늦게 시작한 RViz나 CLI(Command-Line Interface, 명령줄 인터페이스) 구독자도 마지막으로 발행된 모델을 받을 수 있게 한다.

Bash
ros2 node list
ros2 param get /robot_state_publisher robot_description
ros2 topic echo /robot_description --qos-durability transient_local --once

RViz2에서는 RobotModel의 Description Source를 Topic, Description Topic을 /robot_description으로 설정하고 Fixed Frame은 world로 둔다. TF display도 추가한다. parameter 결과는 XML 문자열인지 확인하고, Topic echo가 비면 publisher·토픽 이름·QoS 호환성을 점검한다.

새 터미널에서 joint state와 world에서 link3까지의 변환을 확인한다. link3 형상과 TF 축이 함께 움직이면 가상 관절값에서 시각화까지 연결된 것이다.

Bash
source ~/ros2_ws/install/setup.bash
ros2 topic echo /joint_states --once
ros2 run tf2_ros tf2_echo world link3

로봇이 보이지 않으면 robot_description, joint 이름, Fixed Frame, TF tree 순서로 확인한다.

27.9 Xacro property로 링크 길이와 장착 위치 조정#

앞 절의 3축 로봇을 Xacro 원본으로 바꾸어 길이와 관절 장착 위치를 한곳에서 조정한다. 기존 URDF 파일을 simple_bot_description/urdf/simple_bot.urdf.xacro로 복사하고 robot 요소에 Xacro namespace를 선언한다. 이 실습은 변수의 정의를 반복하기보다, 값 하나를 바꾸었을 때 형상·관절 원점·TF가 함께 바뀌는지 확인하는 데 초점을 둔다.

XML
<robot name="simple_bot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <xacro:property name="base_height" value="0.05"/>
  <xacro:property name="link1_length" value="0.30"/>
  <xacro:property name="link2_length" value="0.40"/>
  <xacro:property name="link3_length" value="0.30"/>
  <!-- 각 link의 cylinder 길이와 joint origin에 같은 property를 사용한다. -->
  <joint name="joint1" type="revolute">
    ... <origin xyz="0 0 ${base_height}" rpy="0 0 0"/> ...
  </joint>
  <joint name="joint2" type="revolute">
    ... <origin xyz="0 0 ${link1_length}" rpy="0 0 0"/> ...
  </joint>
  <joint name="joint3" type="revolute">
    ... <origin xyz="0 0 ${link2_length}" rpy="0 0 0"/> ...
  </joint>
  <!-- link3와 gripper_mount_joint의 길이에도 link3_length를 사용한다. -->
</robot>

... 부분에는 기존 link·joint의 나머지 URDF 요소를 유지한다. link1·link2·link3의 원통 길이와 중심 위치에도 각각 대응하는 property를 연결해야 시각 형상과 관절 원점이 일치한다. gripper_mount_joint의 parent가 link3이면 origin z에 link3_length를 넣는다. 길이를 바꿔도 이름·parent/child 연결은 유지하고, 관성값을 직접 산출하지 않는 실습에서는 제공된 관성 데이터를 그대로 둔다.

Xacro를 일반 URDF로 펼쳐 문법과 링크 트리를 확인한다.

Bash
cd ~/ros2_ws/src/simple_bot_description
xacro urdf/simple_bot.urdf.xacro -o /tmp/simple_bot.urdf
check_urdf /tmp/simple_bot.urdf

실패하면 먼저 namespace, property 이름, ${...} 표현식과 태그 닫힘을 살핀다. check_urdf가 성공해도 RViz에서 형상 위치를 검증해야 한다. property 값은 미터 단위로 유지하고, 링크 길이를 바꿀 때 joint origin과 visual/collision 형상을 함께 갱신한다.

27.10 Xacro를 설치하고 Python Launch에서 robot_description 생성#

패키지 설치 공간에서도 Xacro 원본을 찾을 수 있도록 setup.py의 기존 data_files 항목을 유지하면서 urdf/*.xacro를 추가한다. package.xml에는 실행 의존성 xacro를 선언한다. 이후 launch 파일에서 Xacro를 처리해 일반 URDF XML 문자열을 robot_state_publisher에 전달한다.

Python/XML
(os.path.join("share", package_name, "urdf"), glob("urdf/*.urdf") + glob("urdf/*.xacro")),
<exec_depend>xacro</exec_depend>
Python
from pathlib import Path
import xacro
from ament_index_python.packages import get_package_share_directory

pkg = Path(get_package_share_directory("simple_bot_description"))
xacro_file = pkg / "urdf" / "simple_bot.urdf.xacro"
robot_description = xacro.process_file(str(xacro_file)).toxml()
# Node(..., parameters=[{"robot_description": robot_description}])

기존 display_rviz.launch.py를 바탕으로 display_xacro.launch.py를 만들고 robot_state_publisher의 robot_description parameter에 변환 결과를 전달한다. RViz2와 joint_state_publisher_gui 구성은 앞 절의 실습과 동일하게 유지한다. setup.py에서는 URDF와 Xacro 파일을 모두 설치 대상으로 포함한다.

Bash
cd ~/ros2_ws
colcon build --symlink-install --packages-select simple_bot_description
source install/setup.bash
ros2 launch simple_bot_description display_xacro.launch.py

모든 관절을 0 rad로 두고 TF를 확인한다. 기본 길이가 base_height 0.05 m, link1 0.30 m, link2 0.40 m, link3 0.30 m이면 world 기준 link3 원점은 z=0.75 m, gripper_base_link 원점은 z=1.05 m다. link3와 gripper_base_link 사이 상대 변환은 z=0.30 m다. property를 바꿔 다시 실행하면 그 차이가 TF에 반영되는지 확인한다.

Bash
ros2 run tf2_ros tf2_echo world link3
ros2 run tf2_ros tf2_echo world gripper_base_link
ros2 run tf2_ros tf2_echo link3 gripper_base_link

값이 예상과 다르면 launch가 설치된 Xacro 원본을 읽는지, 모든 관절 원점이 property를 사용하는지, 관절이 0 rad인지 확인한다. 이 확인은 모델 파라미터 변경이 URDF 구조와 TF에 연결되는지 검증하는 절차다.

27.11 Xacro include로 카메라 모듈을 조립한다#

앞 절의 simple_bot 모델과 camera 모듈을 각각 Xacro 파일로 나눈 뒤 include하여 한 로봇 설명 파일로 조립한다. camera.urdf.xacro는 camera_module 매크로를 정의하고, simple_bot_gazebo.urdf.xacro는 기존 로봇 모델의 property·link·joint 정의를 simple_bot_module 매크로 안에 둔다. 두 파일 모두 최상위 robot 요소는 하나만 유지한다.

XML
<!-- camera.urdf.xacro -->
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
  <xacro:macro name="camera_module" params="parent x y z yaw">
    <link name="camera_link">
      <visual><geometry><box size="0.08 0.12 0.08"/></geometry></visual>
      <visual>
        <origin xyz="0.05 0 0"/>
        <geometry><box size="0.02 0.04 0.04"/></geometry>
      </visual>
    </link>
    <joint name="camera_mount_joint" type="fixed">
      <parent link="${parent}"/><child link="camera_link"/>
      <origin xyz="${x} ${y} ${z}" rpy="0 0 ${yaw}"/>
    </joint>
  </xacro:macro>
</robot>

camera_link에는 본체와 렌즈 모양 visual을 둔다. fixed joint는 camera_link를 parent에 고정하며, x·y·z는 parent 좌표계 기준 장착 위치, yaw는 Z축 회전각이다. 매크로 parameter로 위치와 방향을 바꾸면 동일한 모양의 카메라 요소를 서로 다른 좌표에 배치할 수 있다.

simple_bot_gazebo.urdf.xacro의 기존 robot 루트 바로 아래와 닫는 태그 앞에 macro 열기·닫기 태그를 두어 기존 모델 요소를 simple_bot_module 안에 감싼다. 새 robot 루트를 중첩해 넣지 않는다. 최종 파일에서는 두 모듈을 include하고 각각 한 번씩 호출한다.

XML
<!-- simple_bot_camera.urdf.xacro -->
<robot name="simple_bot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <xacro:include filename="simple_bot_gazebo.urdf.xacro"/>
  <xacro:include filename="camera.urdf.xacro"/>

  <xacro:simple_bot_module/>
  <xacro:camera_module parent="base_link"
      x="0.5" y="0" z="3.0" yaw="${pi}"/>
</robot>

실습 파일 세 개를 같은 urdf 디렉터리에 두고 setup.py의 data_files에 *.xacro를 설치한다. parent="base_link"이면 camera는 로봇의 base_link에 고정되며, x=0.5 m와 z=3.0 m는 base_link 기준 장착 위치다. world에 고정된 외부 카메라가 필요하면 parent를 world로 바꾸고 위치를 world 기준으로 다시 지정한다. yaw가 pi이면 Z축 기준 180° 회전한다.

조립 결과를 일반 URDF로 펼친 뒤 XML과 link/joint tree를 확인한다. `camera_link`와 `camera_mount_joint`가 생성됐는지, parent가 의도한 좌표계인지, 두 visual 요소가 올바른 방향인지 RViz2에서 점검한다.

Bash
cd ~/ros2_ws/src/simple_bot_description
xacro urdf/simple_bot_camera.urdf.xacro -o /tmp/simple_bot_camera.urdf
check_urdf /tmp/simple_bot_camera.urdf

camera_module을 재사용할 때마다 link와 joint 이름이 서로 겹치지 않게 해야 한다. 여러 카메라를 만들 경우 매크로에 이름 접두어도 parameter로 추가하거나 Xacro property로 고유 이름을 생성한다. 이는 같은 module definition을 여러 장착 위치에서 사용할 때의 이름 충돌을 막는다.

장 핵심 정리#

  • 1축은 Z축 회전, 2·3축은 같은 Y축 회전으로 구성해 link와 TF의 관계를 확인한다.

  • URDF의 visual/collision/inertial은 서로 다른 목적을 가진다.

  • robot_state_publisher는 XML robot_description과 joint state로 TF를 계산하며, 모델 문자열은 Parameter와 transient local /robot_description Topic으로 확인할 수 있다.

  • RViz2는 물리 시뮬레이터가 아니라 시각화 도구다.

  • Xacro include와 매크로 parameter로 로봇·카메라 모델을 조립하고, 카메라의 parent 좌표계와 회전 방향을 확인한다.

  • Xacro property를 링크 형상과 joint origin에 함께 적용하고, Python Launch에서 생성한 robot_description과 TF를 확인한다.

  • Fixed Frame 문제는 TF tree 문제로 해석해야 한다.

확인 문제#

  1. visual mesh와 collision mesh를 다르게 쓰는 이유를 설명하라.

  2. URDF가 있는데도 RobotModel이 움직이지 않는 원인을 설명하라.

  3. joint_state_publisher와 실제 driver를 동시에 쓰면 어떤 문제가 생길 수 있는가?

  4. link 길이 property를 바꿀 때 원통 형상과 관절 origin을 함께 수정해야 하는 이유를 설명하라.

  5. Xacro 원본 파일을 launch parameter로 직접 넘기지 않고 일반 URDF XML로 변환하는 단계를 설명하라.

  6. joint2와 joint3의 축을 같은 Y축으로 두면 어떤 운동을 만들 수 있는가?

28장. Gazebo·ros2_control·시뮬레이션과 실제 하드웨어의 경계#

28.1 시뮬레이션에서 무엇을 검증할 수 있는가#

시뮬레이션은 실제 장비를 움직이기 전에 ROS(Robot Operating System, 로봇 운영체제) Graph의 Node·Topic·메시지 타입, TF(Transform, 좌표변환) tree, controller 연결, Nav2 설정 및 MoveIt2 경로계획의 논리적 작동을 검증하기 좋다. 하지만 계산된 운동이 실제 하드웨어를 완전히 재현한다는 뜻은 아니다. 마찰·백래시·제동거리·센서 반사와 노이즈·네트워크 지연·컨트롤러 펌웨어·안전정지는 모델 및 현장 조건에 따라 달라지므로, 실물에서 별도의 시험을 거쳐야 한다.

28.2 Gazebo에서 world와 robot model#

Gazebo는 물리 world 안에 좌표계와 robot model(Link·Joint), 물리 엔진 및 센서 플러그인을 배치한다. ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy 환경에서는 Gazebo 자체 통신(Gazebo Transport)의 카메라·LiDAR(Light Detection and Ranging, 광 검출 및 거리측정) 정보가 `ros_gz_bridge`를 거쳐 ROS Topic으로 전달될 수 있다. 데이터가 보이지 않으면 먼저 시뮬레이터 내부 센서가 메시지를 만들고 있는지 확인한 다음 bridge의 Topic 매핑·메시지 타입·QoS(Quality of Service, 서비스 품질)를 점검한다. 물리 시뮬레이션과 ROS 통신 문제를 나누는 것이 핵심이다.

28.3 ros2_control의 역할#

`ros2_control`은 상위 controller와 장비 사이의 명령·상태 인터페이스를 표준화한다. `controller_manager`가 controller plugin을 관리하고, hardware interface가 joint의 position·velocity·effort 등의 상태와 명령을 실제 장비 드라이버 또는 시뮬레이션 하드웨어에 연결한다. 이 경계를 일치시키면 상위 제어 구성을 재사용할 수 있지만, 시뮬레이터에서 controller가 활성화됐다고 실제 장비의 구동·안전 성능까지 검증된 것은 아니다.

계층 예 역할
Command interface position/velocity/effort 상위 controller가 요구하는 명령
State interface joint position/velocity 하드웨어 상태 읽기
Controller joint_trajectory_controller 목표 trajectory를 command로 변환
Hardware plugin 실제 드라이버/가상 하드웨어 장비와 실제 I/O

28.4 “시뮬레이션에서 된다”의 의미를 제한한다#

  • 먼저 `ros2 node list`와 `ros2 topic list -t`로 예상한 Node·Topic과 메시지 타입이 실제로 생성됐는지 확인한다.

  • `ros2 run tf2_tools view_frames`로 TF 연결을 확인하고, `/clock`을 사용하는 시뮬레이션 Node들의 `use_sim_time` 설정을 비교한다.

  • `ros2 control list_controllers`로 controller의 활성 상태를 확인하고, Nav2 또는 MoveIt2 목표를 명령했을 때 계획 결과와 구동 반응을 별도로 기록한다.

  • 이 결과를 실물에 적용하기 전에는 센서 성능·제동거리·통신 장애·보호정지 등을 실제 장비 기준으로 재검증한다.

28.5 Jazzy에서 SDF world를 실행하고 URDF 모델 넣기#

Gazebo Harmonic의 SDF world와 ROS 로봇 URDF는 역할이 다르다. worlds/arm_ready.sdf는 물리 시간 간격, 실시간 배율, Physics·UserCommands·SceneBroadcaster 시스템, 조명과 고정 바닥 모델을 정의한다. URDF는 한 로봇의 link·joint 구조와 visual·collision·inertial을 정의한다. world의 <static>true</static>는 바닥처럼 해당 SDF model을 고정한다는 뜻이며, 로봇의 고정 관절이나 물리 구동 여부를 대신 정하지 않는다.

먼저 world 파일을 열고, 별도 터미널에서 ROS 2 Jazzy의 ros_gz_sim launch를 사용해 URDF를 삽입한다. world 파일명은 arm_ready.sdf, world 이름은 파일 안의 arm_ready와 일치해야 한다.

Bash
# 터미널 1: Gazebo Harmonic world 실행
gz sim "$HOME/ros2_ws/src/simple_bot_description/worlds/arm_ready.sdf"

# 터미널 2: ROS 2 Jazzy에서 URDF 모델 생성
ros2 launch ros_gz_sim gz_spawn_model.launch.py \
  world:=arm_ready \
  file:="$HOME/ros2_ws/src/simple_bot_description/urdf/simple_bot_gazebo.urdf" \
  entity_name:=simple_bot

같은 모델 삽입 작업은 ros_gz_sim의 create executable로도 실행할 수 있다. -world는 대상 world, -file은 URDF 경로, -name은 생성할 entity 이름을 지정한다.

Bash
ros2 run ros_gz_sim create \
  -world arm_ready \
  -file "$HOME/ros2_ws/src/simple_bot_description/urdf/simple_bot_gazebo.urdf" \
  -name simple_bot

두 터미널은 같은 시뮬레이션에 연결되어야 한다. world가 열리지 않으면 SDF 버전과 플러그인 파일명, 파일 경로를 확인한다. 로봇이 나타나지 않으면 launch의 world 이름, URDF 경로, entity_name과 모델의 inertial·collision 정의를 확인한다. URDF를 변경했다면 Xacro 원본에서 최신 URDF를 다시 생성했는지도 살핀다.

ROS 2 Jazzy의 ros_gz_sim은 gz_spawn_model.launch.py와 create executable을 모두 제공한다. gz sim은 Gazebo world를 실행하고, 두 삽입 명령은 이미 실행 중인 world에 robot entity를 추가한다. 어느 방식도 센서 topic을 ROS 2로 연결하는 bridge 설정을 대신하지 않는다.

28.6 시각·충돌·관성 정의와 Gazebo 전용 설정 구분#

URDF에서 <visual>은 화면 모양, <collision>은 충돌 형상, <inertial>은 질량·무게중심·관성을 정한다. 이번 모델은 원기둥 모양을 두 형상에 쓰되, 실제 모델에서는 충돌 계산이 단순해지도록 collision을 visual보다 간략하게 만들 수 있다. <origin>은 각 형상이나 관성값의 link 기준 위치를 지정하므로 서로 다른 origin을 사용할 수 있다.

SDF world의 <physics>, <light>, <model>, <static>, <plugin>은 환경·물리 시스템을 구성한다. URDF의 <gazebo> 확장 요소는 특정 link 또는 joint에 센서·마찰 같은 Gazebo 전용 속성이나 플러그인을 연결할 때 쓴다. 확장 요소를 추가한 뒤에도 기본 URDF의 kinematic tree와 ROS 2 TF 설명은 별도로 유지한다. <static>을 world SDF의 바닥 모델 설정과 혼동하지 않는다.

XML
<gazebo>
  <static>true</static>
</gazebo>

로봇 수준의 <gazebo><static>true</static></gazebo>는 모델 전체를 고정한다. 재생해도 관절이 물리적으로 움직이지 않으므로 형상·카메라 배치 확인에만 사용하고, 관절 제어 실습에서는 제거한다.

Gazebo 안에서 로봇이 보이는 것과 ROS 2에서 센서 메시지를 받는 것은 별도다. 센서가 ROS 2 topic에 나타나야 한다면 시뮬레이터의 센서 system, ros_gz_bridge 설정, 메시지 타입과 QoS를 이어서 확인한다. RViz2는 이 데이터를 보여 주는 시각화 도구이며 Gazebo의 물리 계산을 대체하지 않는다.

장 핵심 정리#

  • Gazebo는 물리환경, RViz는 시각화, ros2_control은 제어 인터페이스 계층이다.

  • SDF world와 URDF robot을 구분하고, gz_spawn_model.launch.py 또는 ros2 run ros_gz_sim create로 기존 world에 robot entity를 넣는다.

  • 상위 ROS 구조를 시뮬레이션과 실제 하드웨어에서 재사용하려면 인터페이스 경계를 명확히 해야 한다.

확인 문제#

  1. RViz2(ROS Visualization 2, ROS 시각화 도구)와 Gazebo의 차이를 “데이터 소비”와 “물리 계산” 관점에서 설명하라.

  2. ros2_control의 controller와 hardware plugin 역할을 구분하라.

  3. Gazebo Harmonic + ROS 2 Jazzy에서 world 실행 명령과 기존 world에 URDF를 넣는 명령의 역할을 각각 설명하라.

  4. URDF의 visual·collision·inertial과 SDF world의 static·physics 요소가 표현하는 대상을 구분하라.

해설 ① USD는 장면 표현, PhysX는 물리 계산이다. ② 시뮬레이션 재생, Bridge 활성화, ROS_DOMAIN_ID·DDS discovery 등을 확인한다. ③ 일반화 좌표·속도·구동기 입력이다. ④ 질량·관성·관절축·충돌 형상·마찰·actuator 설정 등을 확인한다.

확인문제 ① Isaac Sim에서 USD와 PhysX의 역할을 구분하라. ② ROS 2 Bridge가 동작해도 /clock이 보이지 않을 때 확인할 항목을 세 가지 제시하라. ③ MuJoCo의 qpos, qvel, ctrl이 각각 무엇을 나타내는지 설명하라. ④ URDF에서 MJCF로 전환할 때 확인해야 할 물리 파라미터를 나열하라.

Gazebo는 ROS 2 및 이동로봇 내비게이션 학습, Isaac Sim은 GPU 기반 고품질 센서·합성 데이터와 대규모 학습, MuJoCo는 Python 중심 접촉 동역학·로봇팔 제어 실험에 적합하다. 선택 기준은 그래픽 품질보다 검증하려는 물리 현상, 센서 충실도, ROS 2 통합 수준, 하드웨어 자원과 재현성이다.

28.11 Gazebo·Isaac Sim·MuJoCo 선택과 확인문제

MuJoCo 자체가 ROS 2 노드를 자동 생성하는 것은 아니다. rclpy 기반 bridge 노드에서 JointState·TF·센서 메시지를 발행하고 ROS 2 제어 명령을 data.ctrl에 매핑해야 한다. 시뮬레이션 스텝과 ROS 콜백 주기를 분리할 때는 타임스탬프와 스레드 동기화에 주의한다. 실제 로봇으로 이전할 때는 질량·관성·마찰·제어 주기·센서 지연을 비교한다.

28.10.2 ROS 2 연결과 Sim-to-Real

data.ctrl은 actuator 명령, data.qpos는 일반화 좌표, data.qvel은 일반화 속도다. motor의 ctrl은 위치 목표가 아니라 actuator의 제어 입력이며, 실제 토크·힘은 gear와 actuator 설정에 좌우된다. 일정 각도를 유지하려면 위치 actuator 또는 별도 피드백 제어기를 사용한다.

import mujoco

model = mujoco.MjModel.from_xml_path("hinge_demo.xml")
data = mujoco.MjData(model)

for step in range(1000):
    data.ctrl[0] = 0.05
    mujoco.mj_step(model, data)

print("joint position:", data.qpos[0])
<mujoco model="hinge_demo">
  <worldbody>
    <body pos="0 0 0.2">
      <joint name="hinge" type="hinge" axis="0 1 0"/>
      <geom type="box" size="0.1 0.02 0.02" mass="0.2"/>
    </body>
  </worldbody>
  <actuator>
    <motor joint="hinge" gear="1"/>
  </actuator>
</mujoco>

다음 예제는 MuJoCo Python 패키지를 설치한 별도 가상환경에서 실행한다. XML의 motor는 첫 번째 관절에 제어 입력을 가하고, mj_step은 물리 상태를 한 시간 단계 전진시킨다.

28.10.1 Python으로 단일 관절 제어

MuJoCo(Multi-Joint dynamics with Contact, 접촉을 포함한 다관절 동역학)는 접촉·마찰·관절 구동을 계산하는 물리 시뮬레이터다. MJCF(MuJoCo XML Format, MuJoCo XML 모델 형식)는 body, joint, geom, actuator, sensor를 기술한다. URDF를 가져올 수도 있지만 URDF와 MJCF의 물리·구동기 표현은 완전히 같지 않으므로 변환 뒤 관절축, 질량, 관성, 충돌 형상, actuator를 재검토한다.

28.10 MuJoCo — MJCF·동역학·Python 제어

로봇 모델의 USD articulation(관절 연결체)에 카메라·LiDAR를 부착하고 ROS 2 이미지·PointCloud2·JointState 출력을 연결한다. 먼저 RViz2에서 메시지와 frame_id를 확인한 다음, joint trajectory controller에 목표를 보내 시뮬레이션 로봇팔을 움직인다. 카메라 합성 데이터의 조명·재질·노이즈를 바꾸어 인식 모델의 강건성을 실험할 수 있다. Isaac Lab은 강화학습과 병렬 환경 학습을 위한 별도 프레임워크이며, Isaac Sim의 ROS 2 Bridge와 같은 기능으로 취급하지 않는다.

28.9.2 카메라·LiDAR·로봇팔 실습

위 명령은 연결 점검용이다. /clock이 보이지 않으면 시뮬레이터의 재생 상태, Bridge 활성화, DDS discovery 및 ROS_DOMAIN_ID를 확인한다. TF는 map → odom → base_link → sensor 프레임 관계를 검증하고, 동일한 변환을 둘 이상의 노드가 중복 발행하지 않도록 한다.

ros2 topic list
ros2 topic echo /clock --once
ros2 topic list -t
ros2 run tf2_tools view_frames

Ubuntu 및 GPU·드라이버·Isaac Sim의 호환성은 사용하는 Isaac Sim 릴리스의 공식 요구사항을 먼저 확인한다. Isaac Sim 내부에서 ROS 2 Bridge를 활성화하고, 외부 ROS 2 Jazzy 터미널과 ROS_DOMAIN_ID 및 DDS 네트워크 설정을 맞춘다. 시뮬레이션 시간은 /clock을 사용하고 ROS 노드의 use_sim_time을 true로 설정한다.

28.9.1 ROS 2 Jazzy와 연결하는 순서

Isaac Sim은 NVIDIA Omniverse 기반 로봇 시뮬레이터다. USD(Universal Scene Description, 범용 장면 기술 형식)는 로봇과 환경을 장면 그래프로 표현하고, PhysX는 강체·관절·접촉 물리를 계산한다. ROS 2 Bridge는 Isaac Sim과 ROS 2 사이에서 Topic, TF(Transform, 좌표 변환), 센서 메시지를 교환한다.

28.9 NVIDIA Isaac Sim — USD 기반 로봇 시뮬레이션

29장. Twist·차동구동·Odometry#

이 장을 끝내면 Twist와 wheel odometry의 차이를 이해하고 drift 원인을 수치로 설명한다.

29.1 geometry_msgs/Twist#

geometry_msgs/msg/Twist는 선속도 `linear`와 각속도 `angular`를 각각 3축 벡터로 표현한다. 평면 차동구동에서는 보통 `linear.x = v`, `angular.z = ω`를 사용하고 다른 성분은 0으로 둔다. 이 값들은 개별 바퀴 속도가 아니라 로봇 본체의 목표 선속도·각속도이며 하위 controller가 바퀴 속도로 변환한다. 아래 예제는 `/cmd_vel`이 Twist 타입일 때만 사용한다. 24장에서처럼 시스템에 따라 TwistStamped를 쓰기도 하므로 발행 전 `ros2 topic list -t`로 실제 메시지 타입을 확인한다.

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.2}, angular: {z: 0.4}}"

29.2 차동구동 forward kinematics#

좌우 바퀴 속도가 같으면 차이가 0이므로 각속도는 0이고 평균이 선속도가 된다. 반대로 부호가 반대면 평균은 0이고 차이가 커져 제자리 회전한다. 이 관계는 odometry와 cmd_vel 변환의 기본이다.

좌우 바퀴 반지름이 `r`, 바퀴 중심 간 거리(track width)가 `L`, 좌우 바퀴 각속도가 `ω_R`, `ω_L`일 때 로봇 중심의 선속도와 각속도는 다음과 같다.

v=r2(ωR+ωL),ω=rL(ωR−ωL) v=\frac{r}{2}(\omega_R+\omega_L),\qquad \omega=\frac{r}{L}(\omega_R-\omega_L)
where
v로봇 중심의 전진 선속도 [m/s]
ω로봇의 수직축 회전각속도 [rad/s]
r구동 바퀴의 반지름 [m]
L좌우 바퀴 사이 간격 [m]
ω_R, ω_L오른쪽과 왼쪽 바퀴의 각속도 [rad/s]

두 바퀴 속도가 같으면 직진하고, 크기가 같고 부호가 반대면 제자리 회전한다. 이 식은 `cmd_vel`을 wheel speed로 바꾸거나 wheel encoder를 body velocity로 되돌릴 때 기본이 된다.

29.3 pose 적분#

속도를 시간에 대해 적분하면 pose가 된다. 문제는 작은 bias와 slip도 계속 누적된다는 점이다. 그래서 odometry는 부드러운 단기 이동 추정으로 보고, 장기적인 전역 위치는 SLAM/Localization이 보정하는 구조를 사용한다.

ẋ=vcosθ,ẏ=vsinθ,θ̇=ω \dot x=v\cos\theta,\qquad \dot y=v\sin\theta,\qquad\dot\theta=\omega
where
x, y평면상의 로봇 위치 [m]
θ로봇의 heading/yaw [rad]
윗점(·)시간에 대한 미분(예: ẋ = dx/dt)
v, ω로봇 중심 선속도와 각속도

작은 시간 간격 `Δt`마다 적분하면 odometry pose를 갱신할 수 있다. 실제 구현에서는 encoder resolution, time step, wheel slip, 수치적분 오차가 누적된다.

29.4 odom frame의 철학#

`odom`은 점프하지 않는 연속 좌표계, `map`은 전역적으로 일관되지만 보정 때문에 상대적으로 점프할 수 있는 좌표계다. `odom→base_link`와 `map→odom`을 서로 다른 subsystem이 담당하도록 역할을 나누는 이유다.

`odom`은 단기적으로 연속적이고 부드러운 좌표계여야 한다. 바퀴가 조금 미끄러졌다고 pose가 갑자기 점프하지 않는다. 대신 장시간 이동하면 drift가 누적될 수 있다. 반면 `map`은 전역적으로 일관된 위치를 목표로 하며 localization/SLAM 보정 때문에 `map→odom` transform이 천천히 또는 때로는 크게 바뀔 수 있다.

핵심 TF 계약 일반적인 모바일 로봇에서 odometry source가 `odom→base_link`를 제공하고, SLAM/AMCL 같은 global localization이 `map→odom`을 제공한다. 같은 transform을 두 노드가 동시에 publish하면 TF tree가 불안정해진다.

29.5 왜 odometry는 drift하는가#

Odometry drift는 wheel radius/track width calibration, encoder quantization, slip, timestamp, 모델 단순화가 합쳐진 결과다. 특히 yaw의 작은 오차는 앞으로 이동할수록 x/y 오차를 크게 만든다.

  • 바퀴 직경 추정 오차

  • 좌우 wheel scale 차이

  • track width 오차

  • 미끄러짐과 바닥 재질

  • encoder quantization

  • 시간동기화 오차

  • 모델이 실제 동역학을 단순화

특히 heading의 작은 오차는 시간이 지나면 x/y 위치오차로 크게 증폭된다. 그래서 odometry를 절대 위치로 믿지 않고 LiDAR(Light Detection and Ranging, 광 검출 및 거리측정), vision, IMU(Inertial Measurement Unit, 관성측정장치) 등과 결합한다.

29.6 Twist command와 실제 속도는 다르다#

`/cmd_vel`의 Twist는 로봇에 요청한 속도이고, `/odom`의 twist는 센서와 주행 모델에서 추정한 실제 운동이다. 가감속 제한, 모터 포화, 제어 지연, 바닥 미끄러짐 때문에 두 값이 달라질 수 있다. 따라서 명령 속도를 그대로 적분해 위치추정에 사용하지 말고, 동일 시간대의 명령·odometry를 비교해 오차 원인을 구분한다.

장 핵심 정리#

  • Twist는 명령/속도 표현이고, encoder 기반 odometry는 실제 움직임 추정이다.

  • odom은 연속적이지만 장기적으로 drift한다.

  • map→odom과 odom→base_link 역할을 분리해야 한다.

확인 문제#

  1. 좌우 바퀴가 같은 속도로 돌 때와 반대 속도로 돌 때 v, ω가 어떻게 되는지 설명하라.

  2. odom이 전역 위치 기준으로 적합하지 않은 이유를 설명하라.

  3. `cmd_vel`을 그대로 적분해 pose로 쓰면 안 되는 이유를 설명하라.

30장. 센서·공분산·Kalman Filter·robot_localization#

이 장을 끝내면 covariance와 EKF(Extended Kalman Filter, 확장 칼만 필터)를 이해하고 robot_localization 설정을 물리량 기준으로 읽는다.

30.1 센서융합은 “평균내기”가 아니다#

IMU(Inertial Measurement Unit, 관성측정장치)와 wheel odometry가 제공하는 값을 단순 평균하면 측정 대상·측정 모델·신뢰도가 서로 다르다는 사실을 놓친다. 센서융합에서는 먼저 추정할 상태(state)를 정의하고 운동 모델로 다음 상태를 예측(Prediction)한 뒤, 각 센서의 관측 모델과 불확실성(covariance)을 사용해 보정(Update)한다. 따라서 숫자를 평균하는 것이 아니라 상태와 오차를 일관되게 추정하는 과정이다.

그림 30-1. Kalman 계열 필터의 prediction–correction 순환.

30.2 상태벡터와 측정벡터#

상태벡터는 로봇에 관해 알고 싶은 변수이며, 측정벡터는 센서가 관측한 물리량이다. 예를 들어 평면 주행 상태를 `[x, y, θ, v, ω]`로 두면 바퀴 엔코더는 회전량에서 이동·회전 속도를 추정하게 하고 IMU는 각속도와 가속도 등을 측정한다. 일부 IMU 드라이버는 내부 추정으로 orientation도 제공한다. 센서가 직접 관측하지 않은 상태 성분까지 독립 측정값처럼 입력하면 필터에 모순된 정보가 들어간다.

30.3 Covariance의 의미#

공분산(Covariance)은 통계학의 공분산과 같은 개념이다. 두 확률변수가 각각의 평균에서 벗어나는 정도가 얼마나 함께 변하는지 측정한다. 로봇 상태 추정에서는 그 확률변수를 위치값 자체가 아니라 상태 추정 오차나 센서 잡음으로 두기 때문에 공분산을 흔히 “불확실성”의 표현으로 읽는다.

Cov(X,Y)=𝔼[(X−𝔼[X])(Y−𝔼[Y])] \operatorname{Cov}(X,Y)=\mathbb E\!\left[(X-\mathbb E[X])(Y-\mathbb E[Y])\right]
where
X, Y공분산을 계산할 두 확률변수
E[·]기대값, 확률적 평균
Cov(X,Y)X와 Y가 각 평균에서 벗어나는 값들의 곱의 평균

로봇에서는 실제 상태와 추정 상태의 차이를 오차벡터로 놓는다. 이 오차에 통계학의 공분산 정의를 그대로 적용하면 P가 된다. 편향이 없어 E[e]=0이라면 P=E[eeᵀ]로 단순화된다. 단, 상태 추정 오차는 미지의 실제 상태를 포함하므로 P는 필터 모델과 관측을 통해 추정되는 값이다.

𝐞=𝐱−𝐱̂,P=Cov(𝐞)=𝔼[(𝐞−𝔼[𝐞])(𝐞−𝔼[𝐞])T] \mathbf e=\mathbf x-\hat{\mathbf x},\qquad P=\operatorname{Cov}(\mathbf e)=\mathbb E[(\mathbf e-\mathbb E[\mathbf e])(\mathbf e-\mathbb E[\mathbf e])^T]
where
x / x̂실제 상태 / 추정 상태
e두 상태의 차이인 추정 오차벡터
P상태 추정 오차의 공분산행렬
T벡터 전치
E[e]추정 오차의 평균(편향)

평면 위치 오차 (eₓ,eᵧ)의 경우 P의 대각항은 Var(eₓ), Var(eᵧ)이며 비대각항은 Cov(eₓ,eᵧ)이다. 예를 들어 x 위치 오차의 표준편차가 0.2 m라면 그 분산은 0.04 m²이다. 비대각항은 두 오차가 함께 변하는 방향을 나타내므로 단순한 “변수별 오차 크기 목록”과는 다르다.

Kalman 계열 필터에서는 P가 상태 추정 오차의 공분산, Q가 운동·상태전이 모델의 과정 잡음(process noise) 공분산, R이 관측·센서 측정 잡음(measurement noise) 공분산이다. 세 행렬의 수학적 정의는 같지만 어느 오차를 대상으로 하느냐가 다르다. 관측 R을 실제보다 작게 지정하면 그 센서를 과신할 수 있고, 크게 지정하면 관측 반영이 줄어든다. 공분산만으로 센서의 체계적 편향(bias)이 사라지는 것은 아니다.

ROS 2(Robot Operating System 2, 로봇 운영체제 2)의 geometry_msgs/msg/PoseWithCovariance 및 nav_msgs/msg/Odometry pose 공분산은 (x, y, z, roll, pitch, yaw) 순서의 6×6 행렬을 행 우선(row-major) 36개 수치로 저장한다. 대각선의 0·7·14·21·28·35번 원소가 분산에 해당하며 위치 분산 단위는 m², 각도 분산 단위는 rad²이다. sensor_msgs/msg/Imu의 orientation·angular velocity·linear acceleration 공분산은 각각 3×3(9개)이다.

30.4 Kalman Filter와 EKF 직관#

EKF에서 Prediction은 모델이 말하는 다음 상태, Update는 센서가 말하는 실제 관측이다. Kalman gain은 두 정보의 불확실성에 따라 어느 쪽을 얼마나 반영할지 정한다. 수식 암기보다 Q/R/P가 신뢰도를 어떻게 바꾸는지 실험하는 편이 이해가 빠르다.

먼저 29장에서 배운 차동구동의 이동식과 30.2절의 상태벡터를 연결한다. 운동 모델 f는 바퀴 속도·시간 간격으로 새 위치를 예측하고, 관측 모델 h는 그 상태에서 센서가 어떤 값을 볼지 계산한다. F와 H는 각각 이 두 모델을 현 추정값 주변에서 선형화한 행렬이다. 이 연결을 이해한 뒤 아래 식의 예측과 보정을 읽는다.

선형 Kalman Filter는 상태전이와 관측모델이 선형이라는 가정 아래 Gaussian 상태를 예측·보정한다. 로봇의 회전과 비선형 운동모델에서는 Extended Kalman Filter(EKF)가 비선형 함수를 현재 추정점 주변에서 Jacobian으로 선형화한다.

Prediction:𝐱̂k−=f(𝐱̂k−1,𝐮k)Pk−=FkPk−1FkT+QkCorrection:𝐲k=𝐳k−h(𝐱̂k−)Sk=HkPk−HkT+RkKk=Pk−HkTSk−1𝐱̂k=𝐱̂k−+Kk𝐲kPk=(I−KkHk)Pk−(I−KkHk)T+KkRkKkT \begin{aligned}\text{Prediction:}\quad \hat {\mathbf x}^{-}_{k}&=f(\hat {\mathbf x}_{k-1},\mathbf u_k)\\P^{-}_{k}&=F_kP_{k-1}F_k^{T}+Q_k\\\text{Correction:}\quad\mathbf y_k&=\mathbf z_k-h(\hat {\mathbf x}^{-}_{k})\\S_k&=H_kP^{-}_{k}H_k^{T}+R_k\\K_k&=P^{-}_{k}H_k^{T}S_k^{-1}\\\hat {\mathbf x}_{k}&=\hat {\mathbf x}^{-}_{k}+K_k\mathbf y_k\\P_k&=(I-K_kH_k)P^{-}_{k}(I-K_kH_k)^T+K_kR_kK_k^{T}\end{aligned}
where
k / −현재 시간 단계 / 관측으로 보정하기 전의 예측값
x̂, u, z추정 상태, 제어 입력, 센서 관측
f, h운동(상태전이) 모델과 관측 모델
F, H두 모델의 야코비안(Jacobian), 국소 선형화 행렬
P, Q, R상태 추정 오차, 과정 잡음, 관측 잡음의 공분산
y, S, K관측 잔차(innovation), 잔차 공분산, Kalman gain
I, T, −1단위행렬, 전치, 역행렬

여기서 `Q`는 process noise, `R`은 measurement noise다. Q가 크면 모델을 덜 믿고, R이 크면 센서를 덜 믿는 방향으로 해석할 수 있다.

30.5 IMU에서 자주 생기는 함정#

IMU는 설치 방향과 중력, yaw 기준 때문에 실수가 많다. sensor frame이 base_link와 다르면 TF를 정확히 제공하고, orientation이 없는 장비가 0 quaternion을 보내는지, covariance가 미제공임을 올바르게 표시하는지 확인한다.

  • IMU frame 방향이 robot frame과 다름

  • gravity 제거/가속도 해석 오류

  • orientation이 실제로는 미제공인데 covariance를 잘못 표시

  • yaw absolute reference가 없는 IMU에서 장기 heading drift

  • magnetometer가 금속 구조물/모터 주변에서 왜곡

30.6 robot_localization#

`robot_localization`의 EKF/UKF 노드는 Odometry·IMU·Pose·Twist 등 서로 다른 입력을 하나의 상태 추정으로 융합한다. YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)을 작성하기 전에 각 센서가 실제로 측정한 물리량, 기준 좌표계, 측정 시각, 다른 입력과 공유하는 오차 원인을 먼저 구분한다. 그 결과를 바탕으로 `odom0_config`, `imu0_config`에서 사용할 상태 성분을 선택한다.

ekf_filter_node:
  ros__parameters:
    frequency: 30.0
    two_d_mode: true
    odom0: /wheel/odom
    imu0: /imu/data
    odom0_config: [false, false, false,
                   false, false, false,
                   true,  true,  false,
                   false, false, true,
                   false, false, false]
    imu0_config:  [false, false, false,
                   false, false, true,
                   false, false, false,
                   false, false, true,
                   false, false, false]

YAML 설정 해설 이 블록은 Python 코드가 아니라 YAML이다. 들여쓰기가 구조를 결정하며 ros__parameters 아래 값들이 해당 Node의 ROS parameter로 주입된다. odom0_config처럼 boolean 배열을 쓰는 설정은 상태벡터의 어떤 성분을 그 센서에서 사용할지 위치별로 지정하므로 순서를 문서와 함께 확인해야 한다.

중복 정보 주의 wheel odometry가 이미 encoder에서 계산한 yaw를 포함하고 IMU yaw도 넣으면 동일 원인에서 파생된 상관된 정보를 독립 측정처럼 중복 사용하게 될 수 있다. 센서가 실제로 무엇을 측정했는지 추적해야 한다.

30.7 시간 동기화#

로봇이 1 m/s로 이동할 때 센서 측정 시각이 100 ms 어긋나면 위치 대응에 약 10 cm 오차가 생길 수 있다. 여러 PC(Personal Computer, 개인용 컴퓨터)의 IMU·LiDAR(Light Detection and Ranging, 광 검출 및 거리측정)·카메라를 융합할 때는 각 메시지의 timestamp가 어떤 시계를 기준으로 하는지 확인하고 NTP(Network Time Protocol, 네트워크 시간 프로토콜)/PTP(Precision Time Protocol, 정밀 시간 프로토콜)·장치 내부 시각 동기화 정책을 정한다.

장 핵심 정리#

  • 센서융합은 상태·모델·불확실성을 함께 다루는 문제다.

  • 공분산은 통계학의 정의를 그대로 사용한다. P·Q·R은 각각 상태 추정 오차·과정 잡음·관측 잡음의 공분산이며, 대각항(분산)과 비대각항(상호 공분산)을 함께 해석한다.

  • EKF는 비선형 모델을 Jacobian으로 국소 선형화한다.

  • robot_localization 설정은 메시지 타입보다 센서의 물리적 측정 의미에서 시작해야 한다.

확인 문제#

  1. 수학적 공분산 Cov(X,Y)와 Kalman 필터의 추정 오차 공분산 P 사이의 관계를 설명하고, 위치 오차 표준편차 0.2 m의 분산을 계산하라.

  2. Q와 R이 각각 커질 때 필터가 무엇을 덜 믿게 되는지 설명하라.

  3. wheel odom과 IMU에서 같은 yaw 정보를 중복 사용하면 왜 문제가 될 수 있는가?

  4. 100 ms timestamp 오차가 1 m/s 이동 중 위치에 어느 정도 영향을 줄 수 있는가?

31장. 실습 프로젝트 — TF2 장애물 검출기와 RViz Marker#

이 장을 끝내면 LaserScan에서 찾은 장애물 점을 TF2로 base_link에 변환하고 RViz Marker로 시각화하는 Python 노드를 작성한다.

약어 표기 TF2: ROS 2 Transform Library(ROS 2 좌표변환 라이브러리)

31.1 왜 FSM(Finite State Machine, 유한 상태 기계) 다음에 TF(Transform, 좌표변환)인가#

25장의 회피는 scan frame에서 장애물이 가까운지만 알면 된다. 그러나 “장애물이 지도/odom 좌표에서 어디에 있었는가”를 표시하거나 다른 노드와 공유하려면 센서 local 좌표를 공통 frame으로 변환해야 한다. 여기서 TF2가 실제 문제를 해결한다.

31.2 로컬 scan point를 Cartesian point로 바꾸기#

LaserScan의 index `i`에 대한 각도는 `angle_min + i*angle_increment`이고 range를 `r`이라 하면 센서 평면에서 `x=r cosθ`, `y=r sinθ`다. 하지만 이 점은 여전히 `laser_frame` 표현이다. 이동 중인 로봇의 obstacle trail을 안정적으로 그리려면 해당 메시지 timestamp의 `laser_frame → odom` 변환을 적용해야 한다.

31.3 전체 구현#

import math

import rclpy
import tf2_geometry_msgs  # registers geometry message conversions
from geometry_msgs.msg import PointStamped
from rclpy.duration import Duration
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
from tf2_ros import Buffer, TransformException, TransformListener
from visualization_msgs.msg import Marker


class TfObstacleDetector(Node):
    def __init__(self):
        super().__init__('tf_obstacle_detector')
        self.declare_parameter('scan_topic', '/scan')
        self.declare_parameter('fixed_frame', 'odom')
        self.declare_parameter('front_half_angle_deg', 10.0)

        self.fixed_frame = str(self.get_parameter('fixed_frame').value)
        self.front_half_angle = math.radians(
            float(self.get_parameter('front_half_angle_deg').value))

        self.tf_buffer = Buffer(cache_time=Duration(seconds=10.0))
        self.tf_listener = TransformListener(self.tf_buffer, self)
        self.marker_pub = self.create_publisher(Marker, 'obstacle_marker', 10)
        self.scan_sub = self.create_subscription(
            LaserScan,
            str(self.get_parameter('scan_topic').value),
            self.on_scan,
            qos_profile_sensor_data,
        )

    def on_scan(self, scan: LaserScan):
        best = None
        for i, distance in enumerate(scan.ranges):
            angle = scan.angle_min + i * scan.angle_increment
            if abs(angle) > self.front_half_angle:
                continue
            if not math.isfinite(distance):
                continue
            if not (scan.range_min < distance < scan.range_max):
                continue
            if best is None or distance < best[0]:
                best = (distance, angle)
        if best is None:
            return

        distance, angle = best
        p = PointStamped()
        p.header = scan.header
        p.point.x = distance * math.cos(angle)
        p.point.y = distance * math.sin(angle)
        p.point.z = 0.0

        try:
            fixed = self.tf_buffer.transform(
                p, self.fixed_frame, timeout=Duration(seconds=0.05))
        except TransformException as exc:
            self.get_logger().warn(f'TF failed: {exc}')
            return

        marker = Marker()
        marker.header.frame_id = self.fixed_frame
        marker.header.stamp = self.get_clock().now().to_msg()
        marker.ns = 'detected_obstacle'
        marker.id = 0
        marker.type = Marker.SPHERE
        marker.action = Marker.ADD
        marker.pose.position = fixed.point
        marker.pose.orientation.w = 1.0
        marker.scale.x = marker.scale.y = marker.scale.z = 0.18
        marker.color.r = 1.0
        marker.color.a = 1.0
        marker.lifetime = Duration(seconds=0.5).to_msg()
        self.marker_pub.publish(marker)


def main(args=None):
    rclpy.init(args=args)
    node = TfObstacleDetector()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

TF2 Python 해설 tf2_geometry_msgs import는 geometry message 타입에 대한 TF2 변환 지원을 등록해 PointStamped, PoseStamped 같은 메시지를 transform할 수 있게 한다. Duration은 transform을 기다릴 timeout 같은 ROS 시간 길이를 표현한다.

31.4 실행과 RViz 설정#

# simulator 실행 후
source ~/ros2_course_ws/install/setup.bash
ros2 run r2lab_nodes tf_obstacle_detector

# TF 확인
ros2 run tf2_ros tf2_echo odom <scan_frame>
ros2 topic echo /closest_obstacle_marker --once

RViz에서 Fixed Frame을 `odom`으로 두고 Marker display에 `/closest_obstacle_marker`를 추가한다. 로봇이 움직여도 marker가 odom 공간에서 일관된 위치를 가리키는지 관찰한다.

31.5 가장 중요한 비교 실험#

  1. TF 변환을 사용한 원래 코드를 실행하고 로봇을 이동한다.

  2. 임시로 marker의 frame_id를 scan frame으로 두고 변환 없이 같은 숫자를 publish한다.

  3. 두 결과에서 장애물 marker가 “환경에 붙어 있는지” 또는 “로봇과 같이 끌려가는지” 비교한다.

이 비교를 이해하면 Hand-Eye Calibration, object pose transformation, Nav2 map frame의 의미가 한꺼번에 연결된다. 좌표 변환은 API(Application Programming Interface, 응용 프로그램 인터페이스) 사용법이 아니라 “어느 공간 표현을 공유할 것인가”의 문제다.

31.6 TF 오류 진단 표#

증상 먼저 볼 것 대표 원인
lookup timeout frame 이름/TF tree source 또는 target frame이 없음
past extrapolation message stamp와 TF buffer 센서 데이터가 너무 오래됨 / replay clock 불일치
future extrapolation clock와 publisher timestamp 센서가 TF보다 미래 stamp 사용
marker가 로봇과 같이 이동 Fixed Frame / 변환 적용 local point를 global point처럼 사용
벽 위치가 일정하게 offset base→sensor extrinsic 센서 장착 transform 오류

32장. 실습 프로젝트 — VFF 반응형 회피·rosbag2·테스트#

이 장을 끝내면 현장 데이터를 반복 가능한 디버깅 fixture로 만들고 네트워크/지연 문제를 분리한다.

약어 표기 VFF: Virtual Force Field(가상 힘장)

32.1 FSM(Finite State Machine, 유한 상태 기계)에서 연속 제어로 넘어간다#

FSM 회피는 이해하기 쉽지만 상태가 몇 개의 불연속 동작으로 제한된다. VFF(Vector Field Force) 방식은 목표 방향의 인력 벡터와 장애물에서 멀어지는 척력 벡터를 합해 연속적인 진행 방향을 만든다. 이것은 최적의 최신 navigation controller가 아니라, 센서→수학→명령→시각화→테스트의 전체 개발 흐름을 짧게 경험하기 좋은 예제다.

32.2 전체 구현#

import math

import rclpy
from geometry_msgs.msg import TwistStamped
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
from visualization_msgs.msg import Marker, MarkerArray


def clamp(value, low, high):
    return max(low, min(high, value))


class VffAvoidance(Node):
    def __init__(self):
        super().__init__('vff_avoidance')
        self.declare_parameter('influence_distance', 1.2)
        self.declare_parameter('repulsive_gain', 0.08)
        self.declare_parameter('attractive_gain', 0.35)
        self.declare_parameter('max_linear', 0.18)
        self.declare_parameter('max_angular', 1.0)

        self.influence = float(self.get_parameter('influence_distance').value)
        self.k_rep = float(self.get_parameter('repulsive_gain').value)
        self.k_att = float(self.get_parameter('attractive_gain').value)
        self.max_linear = float(self.get_parameter('max_linear').value)
        self.max_angular = float(self.get_parameter('max_angular').value)

        self.cmd_pub = self.create_publisher(TwistStamped, '/cmd_vel', 10)
        self.marker_pub = self.create_publisher(MarkerArray, 'vff_vectors', 10)
        self.scan_sub = self.create_subscription(
            LaserScan, '/scan', self.on_scan, qos_profile_sensor_data)

    def on_scan(self, scan: LaserScan):
        rep_x = 0.0
        rep_y = 0.0
        for i, distance in enumerate(scan.ranges):
            if not math.isfinite(distance):
                continue
            if not (scan.range_min < distance < min(scan.range_max, self.influence)):
                continue
            angle = scan.angle_min + i * scan.angle_increment
            magnitude = self.k_rep * (1.0 / distance - 1.0 / self.influence)
            magnitude /= max(distance * distance, 1e-4)
            rep_x -= magnitude * math.cos(angle)
            rep_y -= magnitude * math.sin(angle)

        att_x, att_y = self.k_att, 0.0
        out_x = att_x + rep_x
        out_y = att_y + rep_y
        heading = math.atan2(out_y, out_x)

        cmd = TwistStamped()
        cmd.header.stamp = self.get_clock().now().to_msg()
        cmd.header.frame_id = 'base_link'
        cmd.twist.angular.z = clamp(1.8 * heading, -self.max_angular, self.max_angular)
        forward_factor = max(0.0, math.cos(heading))
        cmd.twist.linear.x = clamp(out_x, 0.0, self.max_linear) * forward_factor
        self.cmd_pub.publish(cmd)
        self.publish_vectors(att_x, att_y, rep_x, rep_y, out_x, out_y)

    def make_arrow(self, marker_id, x, y, r, g, b):
        marker = Marker()
        marker.header.frame_id = 'base_link'
        marker.header.stamp = self.get_clock().now().to_msg()
        marker.ns = 'vff'
        marker.id = marker_id
        marker.type = Marker.ARROW
        marker.action = Marker.ADD
        marker.pose.orientation.w = 1.0
        marker.scale.x = max(0.02, math.hypot(x, y))
        marker.scale.y = 0.04
        marker.scale.z = 0.04
        marker.pose.orientation.z = math.sin(math.atan2(y, x) / 2.0)
        marker.pose.orientation.w = math.cos(math.atan2(y, x) / 2.0)
        marker.color.r, marker.color.g, marker.color.b = r, g, b
        marker.color.a = 1.0
        return marker

    def publish_vectors(self, ax, ay, rx, ry, ox, oy):
        msg = MarkerArray()
        msg.markers.append(self.make_arrow(0, ax, ay, 0.0, 0.4, 1.0))
        msg.markers.append(self.make_arrow(1, rx, ry, 1.0, 0.1, 0.1))
        msg.markers.append(self.make_arrow(2, ox, oy, 0.1, 1.0, 0.2))
        self.marker_pub.publish(msg)


def main(args=None):
    rclpy.init(args=args)
    node = VffAvoidance()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        stop = TwistStamped()
        stop.header.stamp = node.get_clock().now().to_msg()
        node.cmd_pub.publish(stop)
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

32.3 벡터를 보이지 않게 두지 않는다#

알고리즘이 이상할 때 최종 `/cmd_vel`만 보면 원인을 분리하기 어렵다. 이 예제는 attraction, repulsion, resultant vector를 RViz MarkerArray로 함께 발행한다. 중간 상태를 ROS Topic으로 노출하면 시각화와 rosbag 기록이 쉬워지고, “왜 이런 명령이 나왔는지”를 나중에 재현할 수 있다.

ros2 run r2lab_nodes vff_avoidance
ros2 topic list -t | grep vff
ros2 topic echo /vff_vectors --once

32.4 rosbag으로 현실 입력을 테스트 fixture로 바꾼다#

mkdir -p ~/bags
cd ~/bags
ros2 bag record /scan /odom /tf /tf_static /vff_vectors
# Ctrl+C로 종료
ros2 bag info <생성된_폴더>

rosbag2 해설 ros2 bag record는 Topic 메시지를 timestamp와 함께 기록해 나중에 재생할 수 있게 한다. Ctrl+C로 기록을 정상 종료하면 metadata가 정리된다. ros2 bag play는 기록된 메시지를 다시 publish하므로 실제 센서 입력을 재현 가능한 테스트 fixture처럼 사용할 수 있다.

문제가 발생한 scan을 bag에 남기면 실제 로봇이나 simulator를 다시 같은 위치로 움직이지 않아도 입력을 반복 재생할 수 있다. 알고리즘 노드만 켜고 bag을 재생하면 perception/controller 로직을 독립적으로 디버깅할 수 있다.

# 재생 전에 simulator command 경쟁을 피하고 필요한 노드만 실행
ros2 bag play <bag_directory>

32.5 작은 수학 함수부터 테스트한다#

from r2lab_nodes.vff_avoidance import clamp


def test_clamp_inside():
    assert clamp(0.5, 0.0, 1.0) == 0.5


def test_clamp_low():
    assert clamp(-2.0, 0.0, 1.0) == 0.0


def test_clamp_high():
    assert clamp(3.0, 0.0, 1.0) == 1.0
cd ~/ros2_course_ws
colcon test --packages-select r2lab_nodes
colcon test-result --verbose

테스트 명령 해설 colcon test는 build와 별도로 package에 등록된 test를 실행한다. colcon test-result --verbose는 package별 결과와 실패 내용을 모아 보여준다. 작은 계산 함수는 ROS Graph 없이 unit test하고, Topic/Service/Action 연결은 integration test로 검증하면 원인 분리가 쉽다.

ROS integration test가 중요하더라도 모든 계산을 ROS Node 안에 묻어둘 필요는 없다. clamp, coordinate conversion, scoring 함수처럼 ROS 없이 검증 가능한 부분은 pure function으로 분리하면 빠른 unit test를 만들 수 있다.

32.6 의도적으로 고장내기#

  • repulsion gain을 지나치게 크게 해 로봇이 작은 noise에도 과민하게 회전하는 모습을 본다.

  • scan을 bag으로 기록한 뒤 simulator 없이 알고리즘만 replay하여 결과가 재현되는지 본다.

  • Marker 발행을 끄고 `/cmd_vel` 숫자만 보고 튜닝해 본 뒤, 다시 Marker를 켜서 진단 난이도를 비교한다.

Part VII. SLAM과 Nav2#

33장. Occupancy Grid·Scan Matching·Pose Graph·Loop Closure#

33.1 Mapping, Localization, SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성)의 차이#

세 문제를 섞지 않는다. Mapping은 pose를 알고 지도를 만들고, Localization은 지도를 알고 pose를 찾으며, SLAM은 둘 다 불완전한 상황에서 동시에 추정한다. 실무에서는 odometry가 초기 상대운동 정보를 제공한다.

문제 알고 있는 것 추정하는 것
Mapping 로봇 pose 지도
Localization 지도 로봇 pose
SLAM 둘 다 불완전 지도 + pose

실제로는 odometry라는 상대 이동 추정이 있기 때문에 SLAM이 완전히 무정보 상태에서 시작하는 것은 아니다. 하지만 odometry는 drift하므로 센서 관측으로 반복해서 보정해야 한다.

33.2 Occupancy Grid#

Occupancy Grid는 환경을 일정 크기의 셀로 나누어 각 셀의 점유 확률을 모델링한다. LiDAR(Light Detection and Ranging, 광 검출 및 거리측정) ray가 지나간 셀의 점유 확률은 낮추고 반사 지점은 높이는 식으로 센서 관측을 누적하며, 계산에는 log-odds 형태를 자주 사용한다. 지도 resolution이 너무 작으면 메모리 사용량과 노이즈 민감도가 커지고 너무 크면 좁은 통로나 작은 장애물을 표현하기 어렵다.

l(mi)=logp(mi=1)1−p(mi=1)lt,i=lt−1,i+logp(mi=1∣zt)1−p(mi=1∣zt)−l0,i \begin{aligned}l(m_i)&=\log\frac{p(m_i=1)}{1-p(m_i=1)}\\l_{t,i}&=l_{t-1,i}+\log\frac{p(m_i=1\mid z_t)}{1-p(m_i=1\mid z_t)}-l_{0,i}\end{aligned}
where
mᵢi번째 격자 셀의 점유 여부(1은 점유, 0은 비점유)
p(mᵢ=1)해당 셀이 점유됐을 확률
l점유 확률을 log-odds로 표현한 값
zₜ시간 t의 센서 관측
l₀,ᵢ초기 점유 prior의 log-odds

33.3 Scan Matching#

Scan matching은 현재 LiDAR scan을 이전 scan이나 지도에 정렬하도록 상대 pose(회전·이동)를 최적화하는 방법이다. 보통 odometry 추정값을 시작점으로 두고 alignment score 또는 오차 함수를 개선한다. 긴 복도처럼 특징이 부족한 환경에서는 여러 위치가 비슷한 score를 가질 수 있으므로, 파라미터 튜닝만으로 관측가능성 부족을 해결하기 어렵다.

33.4 Pose Graph#

Pose Graph SLAM에서는 시간순 핵심 pose를 node로 두고, odometry·scan matching·loop closure로 얻은 상대변환을 edge constraint로 연결한다. 모든 제약을 가능한 한 함께 만족하도록 최적화하면 누적 drift가 여러 pose에 나뉘어 수정된다. 그래서 loop closure가 성립한 뒤 지도의 여러 구간이 동시에 조금씩 움직이는 모습은 자연스러운 결과다.

그림 33-1. Pose Graph는 시간순 pose와 상대 제약을 그래프로 표현한다.
X̂=argminX∑(i,j)∈ℰ𝐞ij(X)TΩij𝐞ij(X) \hat X=\underset{X}{\operatorname{argmin}}\sum_{(i,j)\in\mathcal E}\mathbf e_{ij}(X)^T\Omega_{ij}\mathbf e_{ij}(X)
where
X, X̂최적화 대상인 전체 pose 집합과 최적해
E그래프의 연결(제약) 집합
eᵢⱼ두 pose 사이 관측 상대변환과 현재 추정의 잔차
Ωᵢⱼ각 제약의 정보행렬(information matrix), 공분산 역행렬

33.5 Loop Closure#

Loop closure는 로봇이 이전에 방문한 장소를 다시 인식했을 때 현재 pose와 과거 pose 사이에 장거리 제약을 추가하는 과정이다. 올바른 연결은 누적 drift를 줄이지만 다른 장소를 같은 장소로 잘못 연결하는 false positive는 지도를 크게 왜곡할 수 있다. 반복 구조 환경에서는 검출 threshold만 완화하지 말고 센서 정보·주행 경로·검증 조건을 함께 개선한다.

33.6 관측가능성의 한계#

모든 환경에서 LiDAR scan이 로봇 pose의 모든 방향을 충분히 구별해 주지는 않는다. 긴 직선 복도에서는 복도 방향 이동량이 모호해질 수 있고 반복 선반이나 넓은 빈 공간에서도 비슷한 관측이 여러 위치에서 발생한다. 이때는 SLAM 파라미터를 무작정 바꾸기보다 odometry·IMU(Inertial Measurement Unit, 관성측정장치)·환경 특징을 보완해 어떤 자유도에 측정 정보가 부족한지 확인한다.

장 핵심 정리#

  • Occupancy Grid는 cell별 점유 확률 모델이다.

  • Scan matching은 관측을 정렬해 상대 pose를 추정한다.

  • Pose Graph는 여러 상대변환 constraint를 전역 최적화한다.

  • Loop closure는 drift를 고치지만 false match는 전체 지도를 망가뜨릴 수 있다.

확인 문제#

  1. Mapping과 SLAM의 차이를 pose의 알려짐 여부로 설명하라.

  2. Pose Graph에서 node와 edge가 무엇을 의미하는가?

  3. 긴 직선 복도에서 scan matching이 어려울 수 있는 이유를 설명하라.

34장. TurtleBot3 Cartographer와 지도 작성#

이 장에서는 ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy의 ROBOTIS TurtleBot3 기본 SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) 구성인 Cartographer를 기준으로 지도 작성의 원리를 실습 명령에 연결한다. 센서 입력과 좌표계는 33장의 개념을 활용하고, 실물 로봇과 Gazebo 시뮬레이션의 시작 절차를 구분한다. 전체 명령을 함께 실행하는 프로젝트는 41장에서 수행한다.

34.1 TurtleBot3의 기본 SLAM: Cartographer#

SLAM(Simultaneous Localization and Mapping)은 이동 중인 로봇의 위치를 추정하면서 주변의 지도를 함께 만드는 기술이다. TurtleBot3 Jazzy 공식 실습은 Cartographer ROS(Robot Operating System, 로봇 운영체제)를 기본 방법으로 사용한다. Cartographer는 LiDAR(Light Detection and Ranging, 광 검출 및 거리측정)의 거리 관측, odometry(바퀴 등으로부터 얻는 상대 이동량), TF(Transform, 좌표변환)(좌표계 간 변환)를 조합하고 scan matching과 pose graph 최적화로 지도를 개선한다.

세 입력의 역할을 구별한다. /scan은 장애물까지의 거리 측정, /odom과 odom→base_footprint 또는 odom→base_link는 로봇의 연속적인 상대 움직임, TF의 base→sensor 링크는 LiDAR가 로봇 어디에 달렸는지를 나타낸다. 실습에서는 메시지가 도착하는지뿐 아니라 frame_id, timestamp, TF 연결이 서로 맞는지도 확인한다.

ros2 topic list -t | grep -E "scan|odom|map|cmd_vel"
ros2 topic echo /scan --once
ros2 topic hz /scan
ros2 run tf2_ros tf2_echo odom base_link

마지막 tf2_echo는 실제 TF 프레임 이름이 base_link인 구성에서 사용한다. 프레임 이름이 다르면 ros2 run tf2_tools view_frames 또는 ros2 topic echo /tf_static --once로 확인 후 맞춰 바꾼다. 이전에 배운 Topic과 TF 개념을 여기서 센서 입력 계약의 진단 도구로 다시 사용한다.

34.2 두 실행 환경을 구분한다#

실물 TurtleBot3에서는 SBC(Single-Board Computer, 단일보드 컴퓨터)(Raspberry Pi 등)가 구동기·LiDAR를 연결하는 Bringup을 실행하고, Remote PC(Personal Computer, 개인용 컴퓨터)가 Cartographer·RViz2(ROS Visualization 2, ROS 시각화 도구)·키보드 조종을 실행한다. Gazebo 시뮬레이션에서는 Remote PC 안에서 가상 로봇과 센서를 실행하므로 실제 SBC Bringup을 추가로 시작하지 않는다.

  • 실물: SBC에서 ros2 launch turtlebot3_bringup robot.launch.py → Remote PC에서 Cartographer → Teleoperation → 지도 저장
  • 시뮬레이션: Remote PC에서 turtlebot3_gazebo의 월드 실행 → Cartographer(use_sim_time:=True) → Teleoperation → 지도 저장

TURTLEBOT3_MODEL 환경변수에는 burger, waffle, waffle_pi 중 장비에 맞는 값을 지정한다. export는 현재 셸에만 적용되므로 새 터미널에서도 설정해야 한다. 실물 여러 컴퓨터가 통신할 때는 같은 ROS_DOMAIN_ID와 네트워크 통신 가능 상태도 확인한다.

34.3 실제 TurtleBot3: Bringup과 Cartographer#

실물은 SBC를 먼저 켜고 센서가 발행되도록 한다. 다음 명령의 실행 위치가 중요하다. 예제는 Burger 모델이며, 설치·패키지 준비는 ROBOTIS Jazzy Quick Start의 SBC/PC 절차를 선행한다.

# Remote PC에서 SBC 접속; 주소는 실제 장비 IP로 바꾼다.
ssh ubuntu@ROBOT_IP
# 접속된 TurtleBot3 SBC에서
source /opt/ros/jazzy/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_bringup robot.launch.py

SBC 터미널은 계속 실행한 채 Remote PC에 별도 터미널을 연다. 다음은 Remote PC에서 Cartographer를 실행하는 공식 절차다.

# Remote PC
source /opt/ros/jazzy/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_cartographer cartographer.launch.py

SBC의 Bringup은 로봇 하드웨어 인터페이스를 실행하는 역할이고, Remote PC의 Cartographer는 수신한 scan과 TF/odometry를 사용해 지도를 계산한다. 서로 다른 호스트에서 실행되더라도 같은 ROS Graph를 발견해야 하므로 Topic이 보이지 않으면 알고리즘보다 네트워크·도메인·환경 설정부터 확인한다.

34.4 Gazebo에서 Cartographer 실행#

Gazebo는 실제 센서 대신 가상 LiDAR·IMU(Inertial Measurement Unit, 관성측정장치)·odometry와 /clock을 발행한다. ROBOTIS Jazzy 시뮬레이션에서 지도 작성에 사용할 수 있는 TurtleBot3 World 또는 House를 실행한다. 환경 설정과 Gazebo 설치는 ROBOTIS Quick Start/Simulation의 Jazzy 단계를 따른다.

# Remote PC, 터미널 1: Gazebo 세계
source /opt/ros/jazzy/setup.bash
source ~/turtlebot3_ws/install/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# Remote PC, 터미널 2: SLAM (새 터미널에서 동일한 source/export 실행)
ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=True

use_sim_time:=True는 Cartographer가 컴퓨터의 시각 대신 Gazebo가 발행하는 /clock을 사용하도록 하는 Launch 인자다. 센서 메시지 시간과 TF의 기준 시간이 달라지면 extrapolation 오류가 나거나 지도가 갱신되지 않는다. 실물 로봇에서는 일반적으로 위 시뮬레이션 시간 인자를 사용하지 않는다.

34.5 조종하면서 지도 품질을 관찰한다#

# Remote PC, 새로운 터미널
export TURTLEBOT3_MODEL=burger
ros2 run turtlebot3_teleop teleop_keyboard

키보드 조종은 w/x로 선속도를, a/d로 회전속도를 변경하며 Space 또는 s로 정지한다. 급가속·급회전은 scan의 중첩 영역을 줄여 지도 오차를 키울 수 있다. 되도록 모서리와 서로 다른 방향의 벽을 관측하고 출발 지점을 다시 지나면서 loop closure(이전 장소 재인식) 결과를 관찰한다.

  • /scan 발행 빈도와 frame_id 확인: ros2 topic hz /scan, ros2 topic echo /scan --once
  • TF 구조 확인: map→odom→base_link(또는 base_footprint)→base_scan 관계를 구성에 맞게 추적
  • RViz의 /map와 /scan 중첩 확인: 같은 벽이 두 겹으로 갈라지면 TF·odom·시간을 먼저 검토
  • 정지 상태의 scan 흔들림, 루프 재방문의 일치도, CPU(Central Processing Unit, 중앙처리장치) 과부하 여부를 함께 기록

TF의 map→odom은 SLAM이 전역 지도의 오차를 보정할 때 달라질 수 있지만, odom→base의 연속 이동은 하위 odometry가 담당한다. 다음 AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정) 장에서 이 두 변환의 책임이 왜 달라지는지 다시 사용한다.

34.6 저장되는 지도는 두 파일이다#

# SLAM이 실행 중일 때 Remote PC의 다른 터미널
ros2 run nav2_map_server map_saver_cli -f ~/map
ls -lh ~/map.pgm ~/map.yaml

map_saver_cli는 nav2_map_server 패키지의 지도를 파일로 저장하는 실행 프로그램이다. -f ~/map의 -f는 파일 이름의 접두 경로를 지정하며, 보통 ~/map.pgm과 ~/map.yaml이 저장된다. PGM(Portable GrayMap, 그레이스케일 이미지 형식)은 Occupancy Grid의 격자 이미지(점유·빈 공간·미관측 영역), YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)은 image 경로·resolution·origin·threshold 같은 해석 정보를 담는다. map.yaml은 다음 Nav2 단계에서 map:= 경로로 전달한다.

지도 파일 한 쌍은 Nav2에서 고정 지도를 불러올 때 사용하는 결과물이다. Cartographer의 내부 궤적·pose graph 상태와는 구분된다. 지도 저장은 SLAM 노드가 살아 있고 /map을 발행할 때 실행한다.

34.7 OccupancyGrid를 Python으로 직접 발행하기#

저장된 PGM/YAML을 사용하기 전에 지도 메시지의 구조를 작은 격자로 직접 확인한다. nav_msgs/msg/OccupancyGrid는 header, info, data로 구성된다. header.frame_id는 격자가 놓인 좌표계이며 보통 map을 사용한다. info에는 해상도, 가로·세로 셀 수, 격자 원점의 pose가 들어간다.

data는 1차원 배열이지만 2차원 격자를 행 우선(row-major)으로 펼친 값이다. x 방향 셀이 먼저 증가하므로 인덱스는 y × width + x로 계산한다. 배열 길이는 반드시 width × height와 같아야 한다. 관례적으로 -1은 미관측, 0은 빈 공간, 100은 점유 공간을 뜻하며 1~99는 점유 확률을 백분율 범위로 표현한다.

cd ~/ros2_ws/src
ros2 pkg create simple_nav --build-type ament_python --dependencies rclpy nav_msgs

다음 코드를 simple_nav/simple_nav/pub_occupancy_grid.py에 저장한다. 4 × 4 격자이므로 data에는 정확히 16개의 값이 들어간다. create_publisher의 세 번째 인수 10은 KEEP_LAST history에서 보관할 표본 수를 뜻하며, 0.5초 타이머가 같은 격자를 반복 발행한다.

import rclpy
from rclpy.node import Node
from nav_msgs.msg import OccupancyGrid


class OccupancyGridPublisher(Node):
    def __init__(self):
        super().__init__('occupancy_grid_publisher')
        self.publisher = self.create_publisher(
            OccupancyGrid, 'custom_occupancy_grid', 10)
        self.timer = self.create_timer(0.5, self.publish_grid)

    def publish_grid(self):
        msg = OccupancyGrid()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = 'map'
        msg.info.resolution = 1.0
        msg.info.width = 4
        msg.info.height = 4
        msg.info.origin.position.x = 0.0
        msg.info.origin.position.y = 0.0
        msg.info.origin.position.z = 0.0
        msg.info.origin.orientation.w = 1.0
        msg.data = [
            100,  0,   0, 0,
              0, -1,   0, 0,
             -1,  0, 100, 0,
             -1,  0, 100, 0,
        ]
        self.publisher.publish(msg)


def main(args=None):
    rclpy.init(args=args)
    node = OccupancyGridPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

setup.py의 console_scripts 목록에 다음 entry point를 등록한다.

'pub_occupancy_grid = simple_nav.pub_occupancy_grid:main',
cd ~/ros2_ws
colcon build --symlink-install --packages-select simple_nav
source install/setup.bash
ros2 run simple_nav pub_occupancy_grid

다른 터미널에서 인터페이스 정의, 실제 메시지, Publisher 수와 QoS(Quality of Service, 서비스 품질)를 차례로 확인한다. 이 기본 예제는 기본 VOLATILE durability를 사용한다. 지도를 한 번만 발행하는 구조로 바꿀 때 TRANSIENT_LOCAL을 선택하면 늦게 연결한 구독자도 마지막 표본을 받을 수 있지만, 구독 측 QoS가 호환되어야 한다.

ros2 interface show nav_msgs/msg/OccupancyGrid
ros2 topic echo /custom_occupancy_grid --once
ros2 topic info /custom_occupancy_grid -v

RViz에서 Fixed Frame을 map으로 지정하고 Map display의 Topic을 /custom_occupancy_grid로 선택하면 4 × 4 격자를 눈으로 확인할 수 있다. frame_id를 실습용 map_frame으로 바꾸었다면 RViz의 Fixed Frame도 map_frame으로 맞추거나 map과 연결되는 TF를 제공해야 한다. 실제 SLAM의 /map도 같은 메시지 타입을 사용하지만 수천 개 셀과 실제 해상도·원점을 가지며, map_saver_cli는 이 메시지를 PGM/YAML 파일 쌍으로 변환한다.

34.8 Cartographer Lua 튜닝과 점검 순서#

TurtleBot3 Cartographer의 2D 설정은 turtlebot3_cartographer/config/turtlebot3_lds_2d.lua의 Lua 형식 파일에 정의된다. Lua는 Cartographer가 읽는 설정 언어이며 Python 코드를 대체하는 학습 대상이 아니다. 먼저 현 상태에서 센서·TF·시간 오차를 고친 뒤 설정을 하나씩 조정한다.

  • MAP_BUILDER.use_trajectory_builder_2d: 2D trajectory builder 사용 여부
  • TRAJECTORY_BUILDER_2D.min_range / max_range: LiDAR 관측의 사용 거리 범위
  • TRAJECTORY_BUILDER_2D.missing_data_ray_length: max_range보다 먼 관측을 대체할 거리
  • TRAJECTORY_BUILDER_2D.use_imu_data: 2D 추정에 IMU 관측을 사용할지 여부
  • TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching: 실시간 scan matcher 사용 여부
  • TRAJECTORY_BUILDER_2D.motion_filter.max_angle_radians: 작은 회전 관측을 걸러 넣는 기준
  • POSE_GRAPH.optimize_every_n_nodes / constraint_builder.min_score: 전역 최적화 주기와 scan matching 제약 채택 기준

매개변수 이름은 공식 튜닝 지침에 따른다. 예를 들어 max_range를 하드웨어 유효거리 밖으로 늘리는 것보다 먼저 /scan의 range_min/range_max와 실제 관측 품질을 확인한다. 지도가 이중으로 겹치면 Lua 임계값보다 frame·odometry·clock을 먼저 검토한다.

34.9 SLAM Toolbox는 별도의 대안으로 이해한다#

SLAM Toolbox 역시 ROS 2에서 널리 쓰이는 2D LiDAR SLAM 패키지다. online_async_launch.py와 동기/비동기 처리, serialized pose graph 저장·재개 기능은 Cartographer의 TurtleBot3 기본 실행 절차와 다른 별도의 워크플로이다. 비교 실습을 할 때만 패키지와 Launch를 분리해 선택한다. 하나의 TF 트리에서 두 SLAM 구현이 동시에 map→odom을 발행하지 않도록 한다.

# TurtleBot3 공식 Cartographer 실습과 분리된 비교용 예시
sudo apt install ros-jazzy-slam-toolbox
ros2 launch slam_toolbox online_async_launch.py use_sim_time:=True

이 비교용 명령은 SLAM Toolbox 패키지의 일반 Launch 예제이며, TurtleBot3 센서·TF 소스가 이미 올바르게 실행 중이라는 전제가 필요하다. ROBOTIS TurtleBot3의 기본 SLAM 절차는 앞에서 본 Cartographer 명령을 따른다.

장 핵심 정리#

  • TurtleBot3 Jazzy 기본 SLAM은 Cartographer이며, Gazebo에서는 use_sim_time:=True를 사용한다.
  • 실물 SBC Bringup과 Remote PC의 SLAM·Teleoperation 역할을 구분한다.
  • /scan·odom·TF·clock이 지도 품질을 결정하며, 저장한 .pgm/.yaml은 Nav2의 정적 지도 입력이다.

• OccupancyGrid의 data 길이는 width × height이고, 각 값은 미관측·빈 공간·점유 공간을 표현한다.

• 4 × 4 OccupancyGrid의 data 길이와 행 우선 인덱스 계산법을 설명하라.

확인 문제#

  • 실물 로봇과 Gazebo에서 첫 번째로 실행해야 하는 프로그램이 각각 다른 이유를 설명하라.
  • /scan은 정상인데 지도가 이중으로 겹친다면 어떤 입력을 먼저 점검할 것인가?
  • map.yaml과 Cartographer 내부 pose graph 상태의 차이를 설명하라.

35장. AMCL과 지도 기반 Localization#

이 장을 끝내면 정적 지도에서 particle localization을 실행하고 map→odom 관계를 해석한다.

약어 표기 AMCL: Adaptive Monte Carlo Localization(적응형 몬테카를로 위치추정)

35.1 SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) 이후에는 항상 SLAM을 계속 돌려야 하는가#

운영 단계에서는 이미 만든 지도가 안정적이라면 SLAM을 계속 돌릴 필요가 없다. AMCL 같은 localization은 정적 map과 sensor/odom을 이용해 현재 pose만 추정한다. 환경이 실제로 자주 바뀌는 경우에만 lifelong mapping 전략을 별도로 검토한다.

Nav2 운용에서는 Map Server가 앞서 저장한 정적 지도를 제공하고 AMCL이 LiDAR(Light Detection and Ranging, 광 검출 및 거리측정)·Odometry로 지도상의 현재 위치를 계산한다. SLAM 실행 중에 지도를 갱신하는 단계와, 완성된 지도를 사용하는 Localization 단계를 분리해야 실행하는 노드와 map→odom TF(Transform, 좌표변환) 발행 주체가 충돌하지 않는다.

35.2 Particle Filter 직관#

AMCL은 로봇의 위치를 한 점으로 단정하지 않고 가중치를 지닌 여러 pose 후보(Particle, `(x,y,θ)`)를 유지한다. Odometry를 이용한 motion update에서 후보를 이동시키고, 실제 LaserScan이 지도상 그 위치에서 관측될 가능성에 따라 weight를 갱신한다. 이후 resampling으로 가능성이 높은 후보를 더 많이 남긴다. RViz의 particle cloud는 이 가설들의 분포를 눈으로 관찰하는 방법이다.

xt[i]∼p(xt∣ut,xt−1[i])wt[i]∝p(zt∣xt[i],m) \begin{aligned}x_t^{[i]}&\sim p(x_t\mid u_t,x_{t-1}^{[i]})\\w_t^{[i]}&\propto p(z_t\mid x_t^{[i]},m)\end{aligned}
where
xₜ⁽ⁱ⁾시간 t에 i번째 입자가 가정하는 로봇 pose
uₜ운동 입력(odometry 추정)
zₜ센서 관측(LaserScan)
m알고 있는 정적 지도
wₜ⁽ⁱ⁾해당 입자의 관측 적합도에 비례하는 가중치
∼ / ∝분포에서 표본을 뽑음 / 비례 관계

35.3 초기 pose가 필요한 이유#

전역 지도만으로는 로봇이 어느 위치에 있는지 항상 자동으로 유일하게 결정할 수 없다. 반복 구조가 있으면 여러 위치가 비슷하게 보인다. 초기 pose는 particle을 합리적인 영역에 배치해 탐색 공간을 줄이는 정보다.

Global localization은 이론적으로 지도 전체에 particle을 퍼뜨릴 수 있지만 계산량과 모호성이 커진다. 실무에서는 대략적인 초기 위치를 `/initialpose` 또는 RViz “2D Pose Estimate”로 제공해 수렴을 빠르게 한다. 초기 pose가 매우 틀리거나 반복 구조 환경이면 잘못된 위치로 수렴할 수 있다.

35.4 map→odom transform#

`odom→base_link`는 바퀴·IMU(Inertial Measurement Unit, 관성측정장치) 등에서 계산한 연속적인 local odometry를 제공한다. AMCL은 LiDAR와 지도를 이용해 전역 위치 오차를 보정하며, 일반적인 Nav2 TF 구성에서는 그 결과를 `map→odom` 변환으로 발표한다. 따라서 AMCL이 로봇을 실제로 순간이동시키거나 `odom→base_link`를 덮어쓰는 것이 아니다. 두 transform의 발행 주체를 분리해야 TF tree가 안정적이다.

35.5 particle cloud를 읽는 법#

RViz에서 particle cloud가 넓게 퍼져 있으면 여러 위치 가설이 경쟁 중이며, 충분한 이동과 센서 관측으로 좁게 모이면 상대적으로 확신이 높아진다. 그러나 잘못된 대칭 지형에 단단히 수렴할 수도 있으므로 응집만으로 정답을 보장하지 않는다. 로봇을 옮긴 뒤 위치 추정이 틀어지는 kidnapped robot 상황에서는 초기 pose 재설정이나 global localization을 검토한다.

35.6 AMCL tuning에서 먼저 볼 것#

처음부터 particle 수를 크게 늘리지 않는다. map 해상도, LaserScan 품질, odometry, laser model, min/max particles, update_min_d/a 순으로 본다. 입력이 나쁜데 particle만 늘리면 CPU(Central Processing Unit, 중앙처리장치)만 더 쓴다.

  1. 지도 resolution과 LaserScan 품질

  2. base_link↔︎laser TF

  3. odom 품질

  4. laser model 범위와 max beams

  5. motion model이 drivetrain과 맞는지

  6. particle 수를 과도하게 늘리기 전에 sensor/TF 문제 해결

35.7 AMCL YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)을 읽는 순서#

AMCL 설정은 amcl: 아래 ros__parameters:에 둔다. 긴 YAML을 위에서부터 외우기보다 좌표계와 입력 → 운동·센서 모델 → particle 수 → 갱신과 TF → 초기 pose 순으로 읽는다. 먼저 global_frame_id: map, odom_frame_id: odom, base_frame_id: base_footprint, scan_topic: scan이 실제 TF와 Topic 이름에 맞는지 확인한다.

• 운동 모델: robot_model_type은 차동구동·전방향 구동 모델을 구분하고 alpha1~alpha5는 odometry 오차가 회전과 이동에 퍼지는 정도를 나타낸다. 값을 먼저 바꾸기보다 실제 drivetrain과 odometry의 방향·단위를 검증한다.

• Particle 관리: min_particles와 max_particles는 표본 수의 범위다. pf_err와 pf_z는 KLD sampling이 필요한 표본 수를 조절하는 정확도·신뢰도 조건이고, resample_interval은 재표본화 간격이다.

• Laser 모델: laser_model_type, max_beams, sigma_hit, z_hit, z_rand와 최소·최대 사용 거리가 지도와 LaserScan의 일치도를 계산한다. 센서 노이즈나 반사 문제를 가중치만으로 숨기지 않는다.

• 갱신과 TF: update_min_d와 update_min_a는 AMCL이 새 관측을 처리할 최소 이동·회전량이다. transform_tolerance은 map→odom 변환의 시간 허용 범위이고 tf_broadcast는 그 변환을 AMCL이 발행할지 결정한다.

• 초기 pose: set_initial_pose와 initial_pose는 시작 가설을 설정한다. RViz의 2D Pose Estimate를 쓰는 운용에서는 실제 시작 위치와 방향을 지정한 뒤 scan과 map의 겹침을 확인한다.

ros2 param get /amcl min_particles
ros2 param describe /amcl update_min_d
ros2 param dump /amcl > amcl_runtime.yaml

param dump는 실행 중 노드가 실제로 가진 값을 기록한 스냅샷이다. 저장한 파일을 편집하는 것만으로 현재 AMCL이 바뀌지는 않는다. Launch가 그 YAML을 읽도록 지정해 다시 시작하거나, 동적 변경을 지원하는 파라미터만 ros2 param set으로 바꾼다. AMCL은 lifecycle node이므로 구성에 따라 deactivate–configure–activate 또는 전체 Launch 재시작이 필요할 수 있다.

튜닝은 한 번에 한 묶음만 바꾸고 같은 bag 또는 같은 주행 경로로 비교한다. TF 끊김, 잘못된 scan frame, odometry 점프가 있으면 particle 수나 laser 가중치보다 입력 체인을 먼저 고친다.

장 핵심 정리#

  • AMCL은 정적 지도 안에서 pose distribution을 particle로 추정한다.

  • motion update는 odometry, measurement update는 지도와 LaserScan 일치도를 사용한다.

  • AMCL의 전역 보정은 일반적으로 map→odom으로 나타난다.

• AMCL YAML은 frame·Topic, 운동·센서 모델, particle 관리, 갱신·TF, 초기 pose 순으로 점검한다.

• ros2 param dump로 만든 파일을 편집해도 실행 중인 AMCL이 즉시 바뀌지 않는 이유를 설명하라.

확인 문제#

  1. particle filter의 predict–weight–resample 흐름을 설명하라.

  2. 초기 pose가 왜 수렴 속도와 정확도에 영향을 주는가?

  3. AMCL과 wheel odometry가 각각 어떤 TF 역할을 맡는지 설명하라.

36장. Nav2 아키텍처·Costmap·Planner·Controller#

이 장을 끝내면 costmap·planner·controller·BT(Behavior Tree, 행동 트리)의 책임을 분리해 navigation 실패를 진단한다.

36.1 전체 데이터 흐름#

Nav2를 하나의 자율주행 함수로 보지 말고 서버들의 협업으로 본다. localization이 pose를 제공하고 costmap이 환경을 표현하며 planner가 global path를 만들고 controller가 local command를 생성하며 BT Navigator가 이 과정을 task 수준에서 조율한다.

그림 36-1. Nav2의 대표 구성. 실제 구성에는 smoother, route server 등 추가 요소가 있을 수 있다.

사용자가 목표 pose를 보내면 BT Navigator가 전체 navigation task를 조정한다. Planner Server가 global path를 만들고, Controller Server가 path를 추종하는 velocity command를 계산한다. 각 server는 plugin interface를 통해 실제 알고리즘을 교체할 수 있다. global/local costmap은 환경 표현을 제공한다.

36.2 Nav2가 요구하는 기본 계약#

Nav2가 움직이기 전에 TF, odom, sensor, robot footprint, map/localization이 모두 일관되어야 한다. Planner/controller 파라미터를 만지기 전에 이 기초 계약을 검증하지 않으면 증상이 뒤에서 왜곡되어 나타난다.

요소 필요한 것 대표 frame/topic
Localization map 기준 pose map→odom
Odometry 연속 local motion odom→base_link, /odom
Robot model footprint/kinematics base_link
Obstacle sensor LaserScan/PointCloud /scan 등
Velocity control base command interface /cmd_vel
Static map occupancy map /map

TurtleBot3 Jazzy 적용에서는 /cmd_vel의 타입도 계약이다. ROBOTIS 공식 Navigation 문서는 TwistStamped를 사용하므로, 예전 Twist publisher를 연결할 때는 ros2 topic info /cmd_vel -v로 양쪽 타입부터 확인한다. 구체적인 전환 절차는 41.7절에서 실습한다.

36.3 Costmap은 “장애물 지도” 이상이다#

Costmap은 occupancy map을 그대로 쓰는 것이 아니라 static/obstacle/inflation 등 여러 layer를 합성한 navigation용 환경 표현이다. global/local costmap의 크기·frame·update rate가 서로 다른 이유는 계획 범위와 제어 범위가 다르기 때문이다.

Costmap은 각 grid cell에 navigation cost를 부여한다. static layer는 정적 지도, obstacle/voxel layer는 센서 장애물, inflation layer는 장애물 주변에 거리 기반 비용장을 만든다. robot footprint가 들어가므로 단순 점 로봇 최단경로가 아니라 실제 크기를 고려한다.

Global vs Local Costmap#

Global costmap은 넓은 영역에서 path planning에 쓰이고, local costmap은 로봇 주변의 최근 센서 정보를 바탕으로 controller가 즉시 충돌을 피하고 path를 추종하는 데 사용된다. 두 costmap의 크기, update frequency, obstacle source는 같을 필요가 없다.

36.4 Inflation의 직관#

Inflation은 장애물을 단순히 크게 만드는 것 이상의 역할을 한다. 장애물 가까이 갈수록 cost를 높여 planner/controller가 자연스럽게 여유 거리를 선호하게 한다. footprint와 inflation_radius/cost_scaling_factor를 함께 봐야 한다.

장애물 cell 바로 옆만 금지하고 나머지를 모두 같은 free cost로 두면 planner가 벽에 바짝 붙은 경로를 만들 수 있다. Inflation layer는 장애물에서 멀어질수록 감소하는 비용을 부여해 안전 여유가 있는 경로를 선호하게 만든다. `inflation_radius`, `cost_scaling_factor`는 로봇 크기와 통로 폭을 고려해 함께 튜닝한다.

36.5 Planner Server#

Planner Server는 현재 pose에서 목표까지 전역 경로(global path)를 계산한다. Nav2는 플러그인 구조로 NavFn, Smac 계열 등 격자·Hybrid-A*·state-lattice 계열의 계획기를 선택할 수 있다. 로봇의 차동구동·회전반경 같은 운동학 제약, 계산량, 경로 품질에 따라 적합한 선택이 달라진다. 먼저 경로가 유효하게 생성되는지 확인한 뒤 추종 문제와 분리한다.

36.6 Controller Server#

Controller Server는 전역 경로와 local costmap을 참고하여 실제 이동 속도 명령을 계산한다. DWB, Regulated Pure Pursuit, MPPI 등 제어기마다 방식이 다르며, footprint·최대 속도·가감속 제한·제어 주기·odometry 품질과 함께 튜닝해야 한다. 경로는 정상인데 흔들림·코너 오버슈트·좁은 통로 정지가 발생한다면 planner뿐 아니라 controller와 로봇 운동 제약을 확인한다.

Controller 성격 장점 주의
DWB 전통적 local trajectory 평가, 설정 많음 critic tuning 복잡
Regulated Pure Pursuit 경로 추종 직관적, 서비스 로봇에 실용적 경로 품질 의존
MPPI predictive sampling 기반, 복잡한 비용함수 가능 계산량/파라미터 이해 필요

36.7 Planner와 Controller 문제를 분리한다#

목표 이동이 실패하면 즉시 controller 파라미터를 바꾸지 않는다. 먼저 RViz에서 global path가 생성되고 장애물을 피해 합리적으로 이어지는지 본다. 경로 자체가 없거나 벽을 관통한다면 Planner Server·global costmap·TF(Transform, 좌표변환)를 점검한다. 경로는 정상인데 로봇이 따라가지 못한다면 Controller Server·local costmap·odometry·속도 및 가감속 제한부터 조사한다.

공식 문서 연결: Nav2 Jazzy 공식 Navigation Servers, Plugins, Costmap/Controller 설정 문서를 기준으로 plugin 역할을 확인한다.

장 핵심 정리#

  • Nav2는 서버와 plugin으로 구성된 framework다.

  • Costmap은 지도·센서·footprint를 navigation cost로 통합한다.

  • Planner는 path를 만들고 Controller는 velocity command로 추종한다.

  • 문제 진단은 global path 생성과 local path tracking을 분리해야 한다.

확인 문제#

  1. global costmap과 local costmap의 역할 차이를 설명하라.

  2. inflation이 너무 작을 때와 너무 클 때 각각 어떤 문제가 생길 수 있는가?

  3. global path는 정상인데 로봇이 진동한다면 어떤 계층을 우선 확인할지 설명하라.

37장. Behavior Tree·Lifecycle·Bringup·튜닝#

이 장을 끝내면 BT(Behavior Tree, 행동 트리)의 SUCCESS/FAILURE/RUNNING과 recovery 흐름을 읽는다.

37.1 Behavior Tree가 필요한 이유#

Behavior Tree는 경로계획이나 주행제어 알고리즘을 대신하지 않는다. 상위 작업 단계에서 계획→주행→실패 감지→복구→재계획의 실행 순서를 구성한다. Nav2의 Planner·Controller Server를 그대로 사용하면서 상황에 따른 분기와 재시도를 트리로 명시한다.

앞서 FSM(Finite State Machine, 유한 상태 기계)에서 상태 전이로 처리했던 장애물 감지·재시도·복구를, BT에서는 Sequence와 Fallback 노드로 조합한다. 목표가 바뀌면 기존 작업을 취소하고 새로운 경로계획을 요청하는 흐름까지 트리에서 표현할 수 있다.

37.2 BT node의 세 결과#

BT 노드는 매 tick에서 SUCCESS(완료), FAILURE(실패), RUNNING(진행 중) 중 하나를 반환한다. RUNNING 상태를 반환한 뒤 다음 tick까지 기다리는 방식은 앞서 학습한 비동기 ROS(Robot Operating System, 로봇 운영체제) Action과 연결된다. 콜백 내부에서 동기 대기로 막히지 않도록 설계한다.

Sequence는 앞 자식이 SUCCESS를 반환해야 다음 단계를 진행하고, Fallback은 앞 자식이 FAILURE를 반환해야 다음 대안을 시도한다. Nav2는 ComputePathToPose, FollowPath, ClearCostmap, Spin 등의 행동을 트리에 조합한다. 둘 중 하나라도 RUNNING이면 현재 tick에서 무리하게 다음 행동으로 넘어가지 않는다.

Goal received
  -> ComputePathToPose
      -> FollowPath
          -> if failure: clear / spin / wait / replan
          -> success: finish

37.3 Replanning과 RateController#

매 tick마다 경로를 재계산하면 planner를 불필요하게 과부하시킬 수 있다. Nav2의 RateController와 같은 decorator는 planning subtree의 호출 빈도를 제한하고, 그 사이 controller가 기존 경로를 추종하도록 한다. 따라서 BT의 tick 주기와 실제 planner 호출 주기를 구분해서 관찰해야 한다.

37.4 Contextual recovery#

Recovery는 실패하면 회전 하나가 아니라 실패 원인에 따라 달라져야 한다. planner 실패와 controller stuck, localization 문제는 서로 다른 recovery가 필요하다. BT가 문맥 의존 복구를 구조화하는 이유다.

Planner 실패에는 global costmap 정리가 관련 있고, Controller 실패에는 local costmap 정리가 더 관련될 수 있다. recovery를 모든 오류에 동일하게 적용하기보다 실패가 발생한 subsystem 문맥에 맞춰야 한다.

37.5 Lifecycle Manager#

Nav2의 Lifecycle Manager는 여러 server를 configure→activate 순서로 올리고 종료 시 상태를 정리한다. 특정 서버가 Active 상태에 이르지 못하면 프로세스가 실행 중이어도 navigation 요청이 실패할 수 있다. Bringup 실패 시 `ros2 lifecycle nodes`와 `ros2 lifecycle get <node>`으로 각 서버의 현재 상태 및 전환 실패 원인을 확인한다.

37.6 새 로봇을 Nav2에 붙이는 순서#

새 로봇을 Nav2에 붙일 때는 TF→URDF/SDF→odometry→sensor→mapping/localization→footprint→planner/controller 순으로 진행하면 문제가 어디서 생겼는지 추적하기 쉽다. 이 순서 자체가 디버깅 전략이다.

  1. TF(Transform, 좌표변환) tree가 완전한지 검증: map/odom/base_link/sensor 관계

  2. Odometry와 robot base velocity가 정상인지 검증

  3. LaserScan/PointCloud가 올바른 frame과 timestamp로 보이는지 검증

  4. URDF(Unified Robot Description Format, 통합 로봇 기술 형식)/footprint를 실제 크기에 맞게 설정

  5. Localization 또는 SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성)을 단독으로 검증

  6. Global/Local costmap을 RViz에서 검증

  7. Planner로 path만 검증

  8. Controller로 path tracking 검증

  9. 마지막에 BT/recovery와 상위 task를 조정

튜닝 원칙 아래 계층이 틀렸는데 위 계층 파라미터를 만지지 않는다. TF가 틀린 상태에서 costmap inflation을 조절하거나, odometry가 틀린 상태에서 controller critic을 조절하면 문제를 숨길 뿐 해결하지 못한다.

37.7 Controller Server의 주기와 soft real-time#

Controller는 global path와 local costmap을 바탕으로 실제 속도 명령을 만든다. 주행 흔들림, 코너 오버슈트, 좁은 곳 정지 같은 문제는 planner보다 controller/kinematic limits/odometry 문제일 수 있다.

Controller는 일정 주기로 control command를 계산해야 하므로 CPU scheduling과 callback 지연의 영향을 받는다. Nav2 Controller Server에는 controller frequency 같은 timing parameter가 있고, 환경에 따라 soft real-time priority를 고려할 수 있다. WSL2 학습 환경에서 hard real-time을 기대해서는 안 되며 실제 robot controller의 hard real-time 제어와 Nav2의 high-level control loop를 구분해야 한다.

37.8 증상 기반 튜닝#

튜닝은 parameter 이름 암기가 아니라 증상→계층→물리량으로 접근한다. 벽 근처를 너무 멀리 피하면 inflation, path는 좋은데 좌우로 흔들리면 controller/odom, goal 근처에서 계속 도는 경우 goal checker/yaw tolerance를 의심한다.

증상 먼저 볼 것 그 다음
벽에 너무 가까움 footprint/inflation planner cost
제자리에서 진동 odom/TF/controller velocity min/max, critic
목표 근처에서 끝나지 않음 goal checker localization noise
경로가 아예 없음 global costmap/planner map/footprint
장애물 앞에서 늦게 반응 sensor latency/local costmap controller frequency

공식 문서 연결: Nav2 Jazzy First-Time Robot Setup, Tuning Guide, BT walkthrough, Lifecycle 관련 공식 문서를 함께 본다.

장 핵심 정리#

  • BT는 navigation 알고리즘 위에서 작업의 흐름과 recovery를 조정한다.

  • Lifecycle은 server 준비상태를 명시적으로 관리한다.

  • Nav2 튜닝은 TF→odom→sensor→costmap→planner→controller→BT 순으로 올라가는 것이 안전하다.

확인 문제#

  1. Planner 실패와 Controller 실패에서 서로 다른 recovery가 자연스러운 이유를 설명하라.

  2. BT의 RUNNING 상태가 장시간 로봇 작업에 중요한 이유는?

  3. Nav2 튜닝을 “아래에서 위로” 해야 하는 이유를 설명하라.

38장. Nav2 Python API와 실전 시나리오#

이 장을 끝내면 GUI(Graphical User Interface, 그래픽 사용자 인터페이스) 없이 goal·waypoint·cancel을 Python으로 제어한다.

약어 표기 API: Application Programming Interface(응용 프로그램 인터페이스)

38.1 Nav2를 GUI 클릭으로만 사용하지 않는다#

RViz는 훌륭한 관찰도구지만 mission logic을 GUI 클릭에 묶으면 자동화·테스트가 어렵다. Python Simple Commander나 직접 Action client를 사용하면 목표 전송, feedback, cancel, timeout, recovery를 프로그램 상태로 다룰 수 있다.

RViz의 “Nav2 Goal”은 학습에 편하지만 실제 응용에서는 상위 프로그램이 goal을 보내고 결과를 감시해야 한다. Nav2 Simple Commander API(`nav2_simple_commander`)는 Python에서 navigation task를 구성하는 고수준 인터페이스를 제공한다.

from nav2_simple_commander.robot_navigator import BasicNavigator

navigator = BasicNavigator()
navigator.waitUntilNav2Active()
navigator.goToPose(goal_pose)

while not navigator.isTaskComplete():
    feedback = navigator.getFeedback()
    # 진행상황, timeout, 외부 조건 처리

result = navigator.getResult()

Nav2 Python API 해설 BasicNavigator는 Nav2의 여러 Action/Service를 Python 메서드로 감싼 client helper다. goToPose()는 navigation task를 시작하고, isTaskComplete()/getFeedback()로 진행 상태를 polling하며 cancelTask()로 취소할 수 있다. 긴 작업을 blocking 함수 하나로 다루지 않는 것이 핵심이다.

38.2 Waypoint task와 mission layer#

Waypoint 순찰은 단순 목표 배열 이상이다. 각 지점에서 사진 촬영·검사·대기 같은 task를 실행하고 실패 시 skip/retry/abort 정책을 가져야 mission layer가 된다. Navigation과 mission orchestration을 분리하는 것이 유지보수에 유리하다.

한 점으로 이동하는 것은 navigation framework의 가장 작은 단위다. 실제 mission은 여러 waypoint, 작업 대기, 센서 확인, 사람/장비와의 상호작용을 포함한다. mission logic을 Controller plugin 안에 넣지 말고 상위 task layer에서 Action 결과와 상태를 조합하는 편이 구조적이다.

38.3 취소와 preemption#

Action cancel은 긴 작업을 안전하게 중단하는 기본 수단이다. 새 goal이 왔을 때 현재 goal을 취소할지, 큐에 넣을지, 우선순위로 대체할지 정책을 명확히 해야 한다.

새 goal이 들어오거나 안전 조건이 바뀌면 현재 navigation을 취소하고 다른 행동으로 전환해야 할 수 있다. Action/BT 구조를 이해하면 “함수 하나가 끝날 때까지 기다리는” 식의 blocking mission code를 피할 수 있다.

38.4 실전 시나리오: 순찰 로봇#

순찰 로봇을 설계할 때는 waypoint를 모두 돈다보다 각 waypoint의 완료조건과 실패정책을 먼저 정의한다. 도달 tolerance, 최대 이동시간, 실패 횟수, 사람이 막았을 때의 대기, 복귀 기준을 상태로 만들어야 한다.

  1. 지도와 waypoint 목록 로드

  2. Nav2 활성화 대기

  3. 각 waypoint로 이동 Action 시작

  4. feedback에서 remaining distance와 timeout 감시

  5. 도착하면 센서/카메라 작업 실행

  6. 실패 시 원인 분류 후 제한된 retry

  7. 반복 실패 시 안전 위치 또는 operator handoff

38.5 실패를 상태로 설계한다#

예외를 로그 한 줄로 끝내지 말고 상위 상태로 올린다. `NAV_TIMEOUT`, `LOCALIZATION_LOST`, `GOAL_REJECTED`, `STUCK`처럼 실패 원인을 구분하면 retry와 recovery가 결정론적으로 된다.

mission code에서 `try/except`만으로 robot failure를 처리하지 않는다. goal rejected, canceled, failed, timeout, localization lost, blocked 같은 상태를 명시적으로 정의하고 각각의 정책을 갖는다. 이는 나중에 LLM이 자연어 명령을 구조화된 robot action으로 변환할 때도 중요하다.

장 핵심 정리#

  • Simple Commander는 Python 상위 task에서 Nav2를 제어하기 쉽게 한다.

  • Waypoint mission은 navigation과 mission logic을 분리해야 한다.

  • Action 결과와 실패 상태를 명시적으로 모델링해야 robust한 응용이 된다.

확인 문제#

  1. RViz goal 클릭 방식과 Python mission code의 차이를 설명하라.

  2. mission layer가 controller 내부가 아니라 상위 계층에 있어야 하는 이유를 설명하라.

39장. 실습 프로젝트 — Behavior Tree를 Nav2 행동으로 연결하기#

이 장을 끝내면 앞에서 배운 FSM과 Behavior Tree의 역할을 구분하고, BT의 Sequence·Fallback·Action 흐름을 Nav2 행동에 연결하여 실행 결과와 복구 과정을 검증한다.

39.1 FSM(Finite State Machine, 유한 상태 기계)과 BT를 경쟁 관계로 보지 않는다#

25장의 두 상태(FORWARD·TURN) FSM은 전이 조건이 적을 때 명확하고 강력하다. 행동이 중첩되고 retry/fallback/병렬 조건이 늘어나면 전이 그래프가 복잡해질 수 있다. Behavior Tree는 Sequence, Fallback, Decorator, Action/Condition 같은 노드 조합으로 실행 정책을 트리로 표현한다. 실제 로봇 시스템에서는 BT의 leaf가 내부적으로 FSM, controller, action server를 호출할 수도 있다.

39.2 세 가지 상태만 먼저 확실히#

BT 상태 의미 상위 노드가 하는 일
SUCCESS 현재 leaf/subtree가 목표를 달성 다음 child로 진행하거나 상위에 성공 반환
FAILURE 현재 방법으로 목표 달성 실패 Fallback의 다음 대안 등을 시도
RUNNING 아직 작업 중 다음 tick에서 상태를 다시 확인

39.3 가장 작은 Nav2형 트리#

<root BTCPP_format="4" main_tree_to_execute="MainTree">
  <BehaviorTree ID="MainTree">
    <PipelineSequence name="Navigate">
      <ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/>
      <FollowPath path="{path}" controller_id="FollowPath"/>
    </PipelineSequence>
  </BehaviorTree>
</root>

Behavior Tree XML 해설 이 XML은 일반 프로그램 흐름이 아니라 BehaviorTree.CPP가 읽는 트리 정의다. 각 태그는 BT node이고 {goal}, {path} 같은 중괄호 값은 blackboard의 공유 데이터 port를 가리킨다. 부모 노드가 자식들의 SUCCESS/FAILURE/RUNNING 결과를 조합해 실행 정책을 만든다.

이 트리는 “목표까지 path를 계산하고 그 path를 따른다”라는 최소 흐름만 표현한다. 실제 Nav2 기본 tree에는 recovery, rate control, goal update, costmap clear 등 더 많은 정책이 들어 있다. 먼저 작은 트리를 읽을 수 있어야 큰 XML도 “노드 이름 목록”이 아니라 실행 정책으로 읽힌다.

39.4 Nav2에서 BT가 ROS(Robot Operating System, 로봇 운영체제) Action과 연결되는 위치#

상위 application은 `NavigateToPose` goal을 BT Navigator에 보낸다. BT leaf는 planner/controller/recovery server가 제공하는 action/service를 호출한다. 즉 BT는 모터를 직접 구동하는 코드가 아니라 여러 ROS 2 capability를 어떤 순서와 실패 정책으로 조합할지 결정하는 orchestration layer다.

ros2 action list -t | grep -E "navigate|compute|follow"
ros2 node info /bt_navigator
ros2 node info /planner_server
ros2 node info /controller_server

39.5 읽기 연습: failure를 따라간다#

Navigation이 실패했을 때 “Nav2가 실패했다”고 뭉뚱그리지 않는다. BT Navigator가 goal을 받았는지, planner가 path를 만들었는지, controller가 path를 추종했는지, costmap이 lethal obstacle로 막았는지, recovery가 실행됐는지 단계별로 본다. Behavior Tree를 배우는 실용적 이유는 이 실행 경로를 읽기 위해서다.

39.6 의도적으로 고장내기#

  • 존재하지 않는 planner/controller plugin id를 parameter에 지정했을 때 bringup 단계가 어디서 실패하는지 본다.

  • 목표를 명백히 장애물 내부에 지정해 planner 실패와 controller 실패의 로그 차이를 구분한다.

  • BT XML에서 leaf 순서를 바꿨을 때 어떤 data dependency가 깨지는지 생각한다. 예: path가 없는데 FollowPath를 먼저 호출할 수 없다.

40장. 실습 프로젝트 — Nav2 목표 이동·피드백·취소·순찰#

40.1 먼저 공식 Quickstart와 같은 조건을 만든다#

source /opt/ros/jazzy/setup.bash
ros2 launch nav2_bringup tb3_simulation_launch.py headless:=False

RViz에서 초기 pose가 자동으로 올바르지 않다면 `2D Pose Estimate`로 로봇 시작 위치/방향을 지정한다. AMCL의 particle cloud가 지도상의 실제 위치 주변으로 수렴하는지 본다. 그다음 Nav2 Goal로 가까운 free-space 목표 하나를 먼저 보낸다.

40.2 Navigation이 실제로 어떤 인터페이스인지 확인#

ros2 action info /navigate_to_pose
ros2 lifecycle get /planner_server
ros2 lifecycle get /controller_server
ros2 topic echo /plan --once
ros2 topic list -t | grep costmap

`NavigateToPose`는 단순 좌표 Topic이 아니라 장시간 작업 Action이다. 목표 수락, feedback, result, cancel이 있기 때문에 GUI(Graphical User Interface, 그래픽 사용자 인터페이스), task manager, LLM(Large Language Model, 대규모 언어 모델) interface가 모두 같은 추상화를 사용할 수 있다.

40.3 Python에서 한 목표를 보낸다#

import math

import rclpy
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult


def pose(x, y, yaw, navigator):
    msg = PoseStamped()
    msg.header.frame_id = 'map'
    msg.header.stamp = navigator.get_clock().now().to_msg()
    msg.pose.position.x = float(x)
    msg.pose.position.y = float(y)
    msg.pose.orientation.z = math.sin(yaw / 2.0)
    msg.pose.orientation.w = math.cos(yaw / 2.0)
    return msg


def main():
    rclpy.init()
    nav = BasicNavigator()
    nav.waitUntilNav2Active()

    goal = pose(1.5, 0.5, 0.0, nav)
    nav.goToPose(goal)

    while not nav.isTaskComplete():
        feedback = nav.getFeedback()
        if feedback is not None:
            nav.get_logger().info(
                f'remaining={feedback.distance_remaining:.2f} m')

    result = nav.getResult()
    if result == TaskResult.SUCCEEDED:
        nav.get_logger().info('goal succeeded')
    elif result == TaskResult.CANCELED:
        nav.get_logger().warn('goal canceled')
    else:
        nav.get_logger().error('goal failed')

    nav.destroyNode()
    rclpy.shutdown()


if __name__ == '__main__':
    main()
source ~/ros2_course_ws/install/setup.bash
ros2 run r2lab_nodes nav_to_pose

`BasicNavigator` 메서드는 navigation task를 시작한 뒤 즉시 Python 흐름으로 돌아오는 non-blocking 형태를 지원한다. `isTaskComplete()`를 polling하면서 `getFeedback()`을 읽고, 필요하면 `cancelTask()`를 호출한다. “함수가 반환될 때까지 아무것도 못 하는” 코드를 만들지 않는 것이 상위 application 설계에서 중요하다.

40.4 여러 waypoint를 순찰한다#

import math

import rclpy
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult


def make_pose(nav, x, y, yaw=0.0):
    p = PoseStamped()
    p.header.frame_id = 'map'
    p.header.stamp = nav.get_clock().now().to_msg()
    p.pose.position.x = x
    p.pose.position.y = y
    p.pose.orientation.z = math.sin(yaw / 2.0)
    p.pose.orientation.w = math.cos(yaw / 2.0)
    return p


def main():
    rclpy.init()
    nav = BasicNavigator()
    nav.waitUntilNav2Active()

    waypoints = [
        make_pose(nav, 0.5, 0.0),
        make_pose(nav, 1.5, 0.0),
        make_pose(nav, 1.5, 1.0, 1.57),
        make_pose(nav, 0.5, 1.0, 3.14),
    ]

    nav.followWaypoints(waypoints)
    while not nav.isTaskComplete():
        feedback = nav.getFeedback()
        if feedback is not None:
            nav.get_logger().info(f'current waypoint={feedback.current_waypoint}')

    result = nav.getResult()
    if result == TaskResult.SUCCEEDED:
        nav.get_logger().info('patrol complete')
    else:
        nav.get_logger().error(f'patrol ended with result={result}')

    nav.destroyNode()
    rclpy.shutdown()


if __name__ == '__main__':
    main()
ros2 run r2lab_nodes patrol

제공 좌표는 예제일 뿐이다. 사용 중인 map/world의 free space를 RViz에서 확인해 waypoint를 수정한다. 좌표 숫자보다 `frame_id=map`, orientation quaternion, initial localization, planner/controller 상태가 모두 일관되어야 한다는 점이 더 중요하다.

40.5 실패했을 때 보는 순서#

단계 관찰 대표 문제
Localization map에서 로봇 pose/particle cloud 초기 pose 잘못됨, scan/TF 불일치
Global costmap 장애물/footprint/inflation footprint가 과대, map origin/scan layer 문제
Planner global path 존재 여부 goal unreachable, unknown/lethal cell
Local costmap 로봇 주변 obstacle sensor source/clearing 설정 문제
Controller cmd_vel과 path tracking 속도/가속 제한, critic/plugin tuning
BT/Recovery action/log/BT status 반복 recovery, lifecycle inactive

40.6 취소 실험#

목표를 먼 곳으로 지정한 뒤 task 진행 중 일정 시간이 지나면 `navigator.cancelTask()`를 호출하도록 코드를 수정한다. Result code와 로봇 정지 동작을 확인한다. Action cancel이 실제로 상위 task manager에서 왜 중요한지 몸으로 확인한다.

41장. 실습 프로젝트 — TurtleBot3 Cartographer SLAM에서 Nav2까지#

이 프로젝트는 ROBOTIS TurtleBot3 Jazzy의 공식 실행 순서에 맞춰 지도 작성→지도 저장→정적 지도 기반 위치 추정→목표 주행을 연결한다. 34장은 명령과 입력의 의미를 배웠고, 여기서는 단계별 실행 결과를 확인하는 데 집중한다. 아래 과정은 실제 TurtleBot3와 Gazebo 중 한 경로만 선택해 진행한다.

약어 표기 SLAM: Simultaneous Localization and Mapping(동시 위치추정 및 지도작성)

41.1 실습 환경과 사전 준비#

기본 환경은 Ubuntu 24.04, ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy, TurtleBot3 Burger이다. ROBOTIS Quick Start의 Jazzy PC(Personal Computer, 개인용 컴퓨터) Setup에서 TurtleBot3 패키지, Cartographer, Navigation2와 해당 배포판의 시뮬레이터를 준비한다. 시뮬레이션 패키지의 공식 Jazzy 소스 설치 절차는 아래와 같다. 기존에 clone되어 있으면 중복으로 복제하지 않는다.

# Remote PC, 시뮬레이션 패키지가 아직 없을 때
source /opt/ros/jazzy/setup.bash
cd ~/turtlebot3_ws/src
git clone -b jazzy https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
cd ~/turtlebot3_ws
colcon build --symlink-install
source ~/turtlebot3_ws/install/setup.bash

-b jazzy는 Git 브랜치를 선택하는 옵션이다. colcon build는 src의 ROS(Robot Operating System, 로봇 운영체제) 패키지를 빌드하고 install/setup.bash는 해당 워크스페이스를 현재 터미널에 등록한다. ROBOTIS 문서의 Jazzy 소스와 사용 중인 사전 설치된 패키지 버전을 섞지 않도록 동일한 설치 환경을 유지한다.

41.2 경로 A: 실물 TurtleBot3 Bringup#

실물 TurtleBot3를 사용하는 경우 로봇 SBC(Single-Board Computer, 단일보드 컴퓨터)에서 LiDAR(Light Detection and Ranging, 광 검출 및 거리측정)·모터·TF(Transform, 좌표변환) 입력을 발행하도록 먼저 Bringup을 실행한다. Gazebo는 실행하지 않는다. SBC와 Remote PC에서 같은 ROS_DOMAIN_ID로 통신하며, Remote PC에서 /scan을 볼 수 있는지 확인한다.

# Remote PC에서 로봇 SBC 접속
ssh ubuntu@ROBOT_IP
# 아래는 로그인한 SBC에서 실행
source /opt/ros/jazzy/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_bringup robot.launch.py

이 터미널은 유지한다. Remote PC 새 터미널에서 source /opt/ros/jazzy/setup.bash와 작업공간 source를 적용하고 /scan·/odom을 검사한다. TurtleBot3 SBC가 발행하지 않는 상태에서 Remote PC의 Cartographer만 실행하면 지도는 만들어지지 않는다.

41.3 경로 B: Gazebo TurtleBot3 World#

시뮬레이션 경로에서는 SBC 접속 대신 Gazebo를 Remote PC에서 실행한다. /clock이 생성되므로 Cartographer와 Nav2에 use_sim_time:=True를 전달한다. ROBOTIS는 SLAM 실습 환경으로 World 또는 House를 안내한다.

# Remote PC, 터미널 1
source /opt/ros/jazzy/setup.bash
source ~/turtlebot3_ws/install/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

각 새 터미널에도 TURTLEBOT3_MODEL과 source 설정이 필요하다. ROS 2의 Launch는 패키지 내 여러 Node와 그 파라미터를 한 번에 실행하는 도구이며, Gazebo를 종료하면 가상 센서와 /clock도 사라진다.

41.4 Cartographer로 지도 작성하고 저장하기#

경로 A 또는 B를 준비한 뒤 Cartographer를 별도 터미널에서 실행한다. 두 경로의 명령에서 시뮬레이션 시간 설정만 다르다. 다음은 실행 환경에 맞는 한 줄만 선택한다.

# 실물 TurtleBot3: Remote PC
ros2 launch turtlebot3_cartographer cartographer.launch.py

# 또는 Gazebo: Remote PC
ros2 launch turtlebot3_cartographer cartographer.launch.py use_sim_time:=True

다른 터미널에서 ros2 run turtlebot3_teleop teleop_keyboard를 실행하고 천천히 탐색한다. 앞 절에서 배운 Topic→TF→시간 검사법을 적용해 /scan·/map을 확인한다. 지도가 충분히 만들어지면 Cartographer가 실행 중인 상태에서 저장한다.

# Remote PC, 지도 저장
ros2 run nav2_map_server map_saver_cli -f ~/map
ls -lh ~/map.pgm ~/map.yaml

저장된 map.pgm은 셀의 점유 정보를 이미지로 갖고 map.yaml은 그 이미지를 실제 공간의 좌표·해상도로 읽게 해 준다. Nav2에 전달할 것은 map.pgm이 아니라 map.yaml의 절대 경로($HOME/map.yaml)다. map→odom→base_link TF가 연결되는지도 확인한다.

41.5 SLAM 종료 후 Navigation2 실행#

지도 작성과 지도 기반 위치 추정은 동시에 동일한 map→odom을 발행하면 안 된다. 실물에서는 Cartographer와 teleop을 종료하고 SBC Bringup은 계속 유지한다. 시뮬레이션에서는 ROBOTIS 안내대로 이전 시뮬레이션·SLAM 관련 프로그램을 종료하고 동일한 TurtleBot3 World를 다시 시작한다.

# 시뮬레이션 경로: 새 Gazebo World 실행
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# 다른 터미널: Nav2 (시뮬레이션)
ros2 launch turtlebot3_navigation2 navigation2.launch.py use_sim_time:=True map:=$HOME/map.yaml

# 또는 실물 경로: Remote PC에서 Nav2 실행
ros2 launch turtlebot3_navigation2 navigation2.launch.py map:=$HOME/map.yaml

map:=은 Launch에 전달하는 지도 경로 인자이고, $HOME은 현재 사용자의 홈 경로로 치환된다. Navigation2는 저장 지도와 센서 정보를 사용해 AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정) 위치 추정, costmap, 경로 계획, 속도 제어를 실행한다. 지도 파일이 다른 경로에 있다면 map:= 뒤를 해당 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식) 전체 경로로 바꾼다.

41.6 초기 자세 추정과 목표 주행#

RViz2(ROS Visualization 2, ROS 시각화 도구)에서 2D Pose Estimate를 선택하고 실제(또는 가상) 로봇의 지도 위 위치와 방향을 지정한다. LiDAR 관측이 벽과 맞게 겹치도록 하고 필요하면 잠깐 teleop으로 로봇을 움직여 particle cloud가 수렴하도록 한다. teleop을 반드시 Ctrl+C로 종료한 다음 Navigation2 Goal에서 장애물이 없는 지점을 선택한다. 이 순서는 동일한 /cmd_vel을 여러 제어기가 동시에 발행하는 충돌을 방지한다.

Nav2의 목표는 x·y 위치뿐 아니라 화살표 방향으로 지정한 yaw도 포함한다. 로봇이 움직이기 전 AMCL 위치 추정, 글로벌 경로, local costmap, controller 서버가 준비되어 있는지 확인한다. 새 목표는 Action의 goal이며, 진행 중 feedback과 완료·취소 결과를 받을 수 있다.

41.7 Jazzy의 /cmd_vel: Twist와 TwistStamped 구별#

ROBOTIS TurtleBot3의 Jazzy Navigation 문서는 /cmd_vel에 geometry_msgs/msg/TwistStamped를 사용한다고 명시한다. TwistStamped는 선속도·각속도 값(Twist)에 header(시각·frame)를 추가한 메시지다. 앞서 배운 Twist 예제를 그대로 /cmd_vel로 발행하면 타입이 달라 로봇이 반응하지 않을 수 있다.

ros2 topic info /cmd_vel -v
# 실제 출력에서 publisher/subscriber의 메시지 타입을 먼저 확인한다.

Twist 형식을 사용해야 하는 기존 노드가 있다면 ROBOTIS 안내에 따라 turtlebot3_bringup과 turtlebot3_navigation2 양쪽의 enable_stamped_cmd_vel 파라미터를 모두 false로 변경해 일치시켜야 한다. 이를 기본값처럼 가정하지 말고 현재 배포판에서 확인한다. 이 메시지 타입 점검은 15장의 Publisher·Subscriber에서 배운 계약을 실제 주행 안전 진단에 다시 적용하는 예다.

41.8 Cartographer 진단과 지도 품질 재점검#

  • 지도 미표시: ros2 topic list -t와 /scan·/map의 발행 주체부터 확인
  • 벽이 두 겹: odometry 규모·센서 장착 TF·timestamp·/clock 동기화를 점검
  • Nav2 목표 실패: map YAML 경로 → AMCL initial pose → TF(map→odom→base) → costmap → planner/controller → /cmd_vel 타입 순서로 확인
  • 로봇이 제멋대로 움직임: teleop 등 /cmd_vel 다중 발행, TwistStamped 타입 불일치 여부 확인

Cartographer의 Lua 설정을 바꾸기 전 입력 데이터를 고정하고 한 변수씩 조정한다. 34장에서 확인한 min_range/max_range, use_imu_data, optimize_every_n_nodes 등의 의미를 기억하고, 센서 자체 품질과 TF 결함을 알고리즘 튜닝으로 덮지 않는다.

41.9 완료 기준과 다음 실습#

  • 실물·Gazebo 가운데 사용한 경로의 Launch 순서와 호스트 역할을 설명할 수 있다.
  • Cartographer의 /map을 만들고 ~/map.pgm 및 ~/map.yaml을 저장할 수 있다.
  • SLAM을 중단한 뒤 Nav2에서 AMCL 초기 자세를 지정하고 목표 지점에 도착할 수 있다.
  • Jazzy /cmd_vel의 TwistStamped 여부와 TF·시간·센서 상태를 스스로 점검할 수 있다.

후속 실습에서는 38장과 40장에서 배운 Python BasicNavigator를 사용해 목표 이동·feedback·cancel·순찰을 실행한다. GUI(Graphical User Interface, 그래픽 사용자 인터페이스)에서 성공한 경로를 Python으로 자동화하는 것이 다음 학습 단계다.

Part VIII. 로봇팔·비전 통합#

42장. MoveIt2·Planning Scene·E0509#

이 장을 끝내면 planning scene과 trajectory execution이 driver/controller와 어떻게 연결되는지 이해한다.

42.1 MoveIt2의 역할#

MoveIt2는 로봇팔의 inverse kinematics·motion planning·충돌 검사·Planning Scene·trajectory execution을 조율하는 상위 프레임워크다. 목표 joint 값 또는 end-effector pose에서 충돌을 피하는 trajectory를 생성하고 이를 적절한 controller에 전달한다. 실제 서보 주기와 모터 제어는 robot controller/driver의 책임이므로, 계획 성공이 곧 구동 성공을 의미하지는 않는다.

42.2 URDF(Unified Robot Description Format, 통합 로봇 기술 형식)와 SRDF(Semantic Robot Description Format, 의미론적 로봇 기술 형식)#

URDF는 로봇의 Link·Joint·collision geometry 등 물리적·운동학적 구조를 표현한다. 이에 비해 SRDF는 MoveIt2에 필요한 planning group, end effector, virtual joint, disabled collision pair 같은 의미론적 구성을 추가한다. 예를 들어 여섯 관절 가운데 어느 체인을 manipulator group으로 묶을지를 SRDF에서 정하며, 설정이 어긋나면 planning group이 비거나 불필요한 충돌 판정이 생긴다.

42.3 move_group과 Planning Scene#

`move_group`은 로봇 모델·kinematics(운동학)·planner(경로 계획기)·controller 연결을 조정한다. Planning Scene은 로봇의 현재 상태와 작업대·상자 같은 환경 충돌 객체를 포함한 세계 모델이다. 인식한 물체를 scene에 추가하지 않으면 planner가 그 물체를 무시할 수 있다. 객체를 추가했는데도 경로가 관통한다면 업데이트가 실제 `move_group`의 scene에 반영되었는지 점검한다.

42.4 Joint-space vs Cartesian path#

Joint-space planning은 목표 관절값이나 end-effector pose에 도달하는 충돌 없는 경로를 구성공간(configuration space)에서 찾는다. Cartesian path는 말단장치가 공간에서 직선 등 지정된 경로를 따르도록 waypoint를 보간하며, 집어 올리기나 삽입 작업의 일부 구간에 유용하다. 다만 singularity·joint limit·reachability·충돌로 경로가 부분적으로만 계산될 수 있으므로 모든 동작을 Cartesian으로 통일하지 않는다.

42.5 Controller와 trajectory execution#

MoveIt2가 계획한 trajectory가 실제 E0509를 움직이려면 joint 목록·controller 이름·FollowJointTrajectory 등의 Action 인터페이스와 Doosan ROS 2(Robot Operating System 2, 로봇 운영체제 2) driver/controller 버전이 서로 맞아야 한다. 계획(Planning) 성공과 실행(Execution) 성공은 다른 단계다. 계획 결과는 나오는데 로봇이 정지해 있다면 planner를 재설정하기보다 trajectory execution 연결과 controller state부터 확인한다.

42.6 Doosan ROS 2 환경의 버전 경계#

이 교재는 ROS 2 Jazzy에서 기본 개념과 MoveIt2 계획 흐름을 학습한다. 그러나 E0509를 실제로 연결할 때에는 Doosan 공식 `doosan-robot2` 저장소에서 해당 모델·컨트롤러가 지원하는 ROS(Robot Operating System, 로봇 운영체제) 배포판 및 branch를 확인해야 한다. 특정 공식 예제가 Humble 기준이라면 그것을 Jazzy 명령으로 추측 변환하여 적용하지 않는다. 일반 ROS 2 프로그램이 Jazzy에서 실행된다는 사실과 산업용 로봇 드라이버의 공식 Jazzy 지원은 별개의 문제다.

실무 분리 학습용 ROS2 Jazzy workspace와 실제 E0509 공식 driver workspace를 분리해도 된다. 시스템 간 연결은 네트워크/ROS bridge 또는 지원되는 환경 구성으로 검증하며, 무리하게 모든 것을 한 distro로 합치는 것이 목표는 아니다.

42.7 Safety와 Planning의 경계#

MoveIt2의 충돌 검사는 작업 경로를 계획하는 소프트웨어 기능이지 산업용 로봇의 인증된 안전 컨트롤러가 아니다. 비상정지·보호정지·속도와 힘 제한·안전 I/O·작업공간 제한은 장비 컨트롤러와 현장 안전설계가 맡는다. 계획 결과가 충돌 없이 보인다고 해서 실제 작업의 안전 조건까지 충족했다고 해석하지 않는다.

공식 문서 연결: MoveIt2 Jazzy 공식 문서와 DoosanRobotics/doosan-robot2 공식 저장소를 각각 해당 역할 범위에서 확인한다.

42.8 DART Platform과 Task Writer#

DART Platform은 두산 로봇 컨트롤러에 연결해 로봇 설정, 업데이트, 수동 조작, 작업 프로그램 작성을 수행하는 도구다. Task Writer에서 만든 프로그램은 컨트롤러 가까이에서 실행되는 작업 로직이고, MoveIt2는 외부 컴퓨터에서 충돌을 고려한 경로를 계획하는 상위 계층이다. 두 도구가 모두 로봇을 움직이지만 실행 위치와 책임이 다르므로, 한쪽의 속도·좌표·안전 설정이 다른 쪽에도 자동 적용된다고 가정하면 안 된다.

DART를 로봇에 연결할 때는 컨트롤러 식별 정보에 맞는 IP(Internet Protocol, 인터넷 프로토콜) 주소와 PC(Personal Computer, 개인용 컴퓨터)의 서브넷을 먼저 확인한다. 같은 이더넷에 꽂았다는 사실만으로 통신이 보장되지는 않는다. PC와 컨트롤러가 동일한 서브넷에 있어야 하며, 중복 IP·방화벽·네트워크 어댑터 우선순위도 함께 점검한다. 통합 업데이트 패키지는 대상 모델과 컨트롤러 버전을 확인한 뒤 적용하고, 업데이트 중에는 전원과 네트워크를 끊지 않는다.

DART에서 서보 온(Servo On)은 조인트 구동 전원을 활성화해 로봇을 움직일 수 있는 상태로 전환하는 절차다. 서보가 켜졌다는 사실은 동작 명령을 내려도 안전하다는 뜻이 아니다. 비상정지 버튼이 눌렸거나 안전 제한을 위반하면 서보 오프 또는 보호정지 상태로 바뀔 수 있으므로, 먼저 상태 표시와 정지 원인을 확인한 뒤 작업 공간을 비우고 저속으로 시험한다.

직접 교시(Hand Guiding)는 콕핏 버튼을 누른 상태에서 로봇을 손으로 이끌어 자세를 잡는 방법이고, 조그(Jog)는 DART의 조인트 또는 태스크 축을 선택해 버튼을 누르는 동안만 움직이는 방법이다. 처음에는 수동 속도를 10% 정도로 낮추고, 조인트 조그로 각 관절의 움직임을 확인한 다음 태스크 조그에서 base와 tool 기준 방향을 비교한다. 교시한 점을 저장할 때는 posj인지 posx인지, 기준 좌표계와 단위가 무엇인지 함께 기록한다.

Task Writer의 블록은 단순한 그림이 아니라 DRL(Doosan Robot Language) 명령을 구조화한 표현이다. 모션, 흐름 제어, 힘 제어, 입출력·통신 블록을 순서대로 배치하면 컨트롤러가 실행할 프로그램이 된다. 프로그램의 대표 구조는 다음과 같다.

구역실행 시점용도
GlobalVariables프로그램 전역여러 함수와 블록이 공유할 상태, 위치, 설정값을 선언한다.
CustomCodeMainSub 진입 전 1회통신 채널, 그리퍼, 사용자 함수처럼 본 작업 전에 필요한 초기화를 수행한다.
MainSub작업 본문이동, 대기, 조건, 반복, I/O를 조합해 실제 공정을 수행한다.
EndMainSub본문 종료주 작업의 끝을 나타낸다. 종료 시 안전 위치와 출력 상태를 명시적으로 정리한다.

DRL의 int, float, str, bool, list는 일반 데이터 타입이고, posj와 posx는 각각 관절 자세와 작업공간 자세를 표현한다. posj의 값은 각 관절축의 각도이고, posx는 TCP(Tool Center Point, 공구 중심점)의 위치와 방향이므로 두 타입을 바꾸어 넣을 수 없다. 전역 변수는 프로그램 전체에서 공유되지만 지역 변수는 해당 블록이나 함수 안에서만 유효하다. 위치 이름에 기준 좌표계와 단위를 드러내면 실수 가능성이 줄어든다.

42.9 모션 명령·좌표계·속도·블렌딩#

로봇 모션 명령은 목표점만 정하는 것이 아니라 그 목표점까지 어떤 경로로 갈지 결정한다. 같은 시작점과 끝점이라도 관절공간 경로와 TCP 경로는 중간 동작이 다르다.

명령경로 의미대표 용도와 주의점
MoveJ관절공간 이동각 관절이 목표 각도로 효율적으로 이동한다. 빠르지만 TCP 중간 경로는 직선이 아니므로 장애물과 치구를 확인한다.
MoveLTCP 직선 이동공구 끝이 직선을 따라간다. 삽입·접근·후퇴에 유용하지만 특이점과 관절 한계에 민감하다.
MoveSJ / MoveSX여러 점을 잇는 관절/TCP 경로연속 waypoint 경로를 만든다. 점의 순서와 좌표계가 일관되어야 한다.
MoveC원호 이동중간점과 끝점 또는 원호 조건으로 곡선을 만든다. 세 점이 거의 일직선이면 원호가 불안정해진다.
MoveJX직교좌표 목표의 관절 이동목표는 TCP pose로 주되 선택한 관절 구성으로 관절공간 이동한다. 역기구학 해와 자세 구성을 확인한다.
MoveB구간을 연결한 블렌드 경로여러 선·원호 구간을 부드럽게 잇는다. 접촉 직전에는 위치 정밀도 때문에 블렌딩을 줄이거나 끈다.
MoveSpiral나선 이동회전수, 최대 반경, 축 방향 이동량, 기준 좌표계를 지정한다. 탐색·연마처럼 점진적 접촉에 사용한다.
MovePeriodic주기 이동진폭·주기·축을 지정해 반복 운동을 만든다. 주변 공간과 케이블 간섭을 먼저 확인한다.

절대 이동은 지정한 좌표계의 목표 pose 자체로 이동하고, 상대 이동은 현재 pose에 변위를 더한다. 같은 상대 변위라도 기준이 base frame이면 작업장 고정 방향으로, tool frame이면 현재 공구가 향한 방향으로 움직인다. 예를 들어 tool Z 방향으로 50 mm 후퇴시키려는 동작을 base 기준으로 작성하면 공구 자세가 바뀌었을 때 예상과 다른 방향으로 이동할 수 있다. 명령마다 기준 좌표계를 명시하고, 교시한 위치의 frame과 실행 시 frame이 같은지 확인한다.

posx의 A, B, C는 TCP 방향을 나타내는 오일러 각이다. 두산 로봇의 ZYZ 표현은 고정된 축에 각도를 한 번씩 더하는 값이 아니라 Rz(A) 다음 Ry(B), 다시 Rz(C)를 순서대로 합성한 회전이다. eul2rpy와 rpy2eul은 ZYZ 오일러 각과 XYZ 계열의 roll-pitch-yaw 표현을 변환할 때 사용한다. 변환 전후에 각도 숫자가 달라도 같은 자세를 나타낼 수 있으며, 사용한 회전 순서와 각도 단위를 함께 확인해야 한다.

특이점(Singularity)은 특정 자세에서 작업공간의 한 방향 운동을 관절 운동으로 안정적으로 바꾸기 어려워지는 상태다. 손목 특이점은 J5가 0도 부근일 때, 어깨 특이점은 손목 중심이 J1 축과 나란해질 때, 팔꿈치 특이점은 팔이 펴지거나 접히는 경계 부근에서 나타날 수 있다. MoveL로 특이점을 통과하려 하면 관절 속도가 급격히 커지거나 관절각·속도 제한 오류가 날 수 있다. 이때는 시작 자세나 TCP 방향을 바꾸고 중간 waypoint를 추가하거나, 직선 경로가 꼭 필요하지 않은 구간을 MoveJ로 전환한 뒤 저속으로 다시 검증한다.

속도는 전역 속도와 개별 명령의 지역 속도로 나뉜다. 지역값이 지정되면 보통 해당 명령에 우선 적용되므로, 전체 속도를 낮췄는데 특정 구간만 빠르게 움직이는 상황을 점검해야 한다. 시간 기반 실행은 목표 시간을 맞추도록 속도를 계산하므로 너무 짧은 시간을 주면 속도·가속도 한계에 걸릴 수 있다. 실제 장비에서는 낮은 속도와 넉넉한 가속도로 먼저 검증한다.

블렌딩 반경은 앞 구간이 끝나기 전에 다음 구간을 겹쳐 시작해 정지 없이 부드럽게 연결하는 거리다. 반경이 너무 크면 waypoint를 정확히 통과하지 않고 모서리를 크게 잘라낸다. 반경은 보통 구간 거리의 절반 이하에서 시작해 경로를 검증하되, 실제 허용값과 지원 명령은 컨트롤러 버전·명령 조합에 따라 확인해야 한다. MoveJ, MoveL, MoveC, MoveJX는 블렌딩을 적용할 수 있는 대표 명령이고, MoveB, MoveSJ, MoveSX, MoveSpiral, MovePeriodic는 자체 경로 의미가 있으므로 동일한 반경 옵션을 일괄 적용하지 않는다.

42.10 RH-P12-RN(A) 그리퍼와 RS-485 Modbus RTU(Remote Terminal Unit, 원격 단말 장치)#

RH-P12-RN(A)는 두 손가락이 하나의 구동축으로 함께 움직이는 1-DOF 적응형 그리퍼다. 수동 관절이 물체 형상에 맞춰 접촉을 분산하지만, 물체의 재질·접촉면·가속도에 따라 실제 파지 가능 질량은 달라진다. 제품 사양표의 핵심 값은 다음과 같다.

항목값설계 의미
질량500 gE0509 말단 탑재하중에서 그리퍼 자체 질량을 먼저 차감한다.
스트로크0–106 mm기구의 물리 스트로크와 통신 명령의 내부 단위는 구분한다.
최대 닫힘 속도75 mm/s깨지기 쉬운 물체는 더 낮은 속도와 전류로 접근한다.
최대 파지력170 N최대값을 상시 사용하지 말고 마찰과 물체 강도에 맞춘다.
권장 가반하중5 kg로봇 팔의 속도·자세·가속도 제한과 함께 판단한다.
전원·통신24 V, RS-485전원 극성, 신호선 A/B, 공통 기준, 종단저항을 확인한다.
환경−5–55 °C주변 온도와 연속 운전 발열을 고려한다.

E0509의 정격 가반하중이 5 kg이라면 0.5 kg 그리퍼를 장착한 뒤 물체에 쓸 수 있는 단순 질량 여유는 약 4.5 kg이다. 그러나 이 값은 정적 뺄셈일 뿐이다. TCP에서 멀어진 무게중심, 빠른 가속, 손목 자세가 만드는 모멘트까지 포함해 로봇의 payload와 TCP를 설정해야 한다.

그리퍼 통신은 RS-485의 전기 계층 위에서 Modbus RTU 프레임을 주고받는 방식이다. RS-485는 차동 신호와 멀티드롭 배선을 정의하고, Modbus RTU는 장치 주소, function code, register 주소·값, CRC(Cyclic Redundancy Check, 순환 중복 검사)의 의미를 정한다. 예제는 플랜지 직렬 포트를 57600 bps, 8N1로 설정한다. 8N1은 데이터 8비트, 패리티 없음, 정지비트 1개라는 뜻이며 양쪽 설정이 하나라도 다르면 응답이 깨진다.

Function code의미그리퍼 예
01 / 02Coil / discrete input 읽기비트 단위 상태를 읽는다.
03 / 04Holding / input register 읽기설정값이나 측정 상태를 word 단위로 읽는다.
05 / 06단일 coil / register 쓰기토크 enable이나 단일 목표값을 설정한다.
15 / 16여러 coil / register 쓰기여러 연속 값을 한 프레임으로 갱신한다.

제어 코드의 핵심 holding register는 주소 256의 torque enable, 275의 goal current, 282의 goal position이다. 문서에 40257처럼 표시되는 값은 ‘4xxxx holding register’ 표기이고, 실제 Modbus PDU(Protocol Data Unit, 프로토콜 데이터 단위)에는 0부터 시작하는 주소 256이 들어갈 수 있다. 라이브러리와 장치 매뉴얼이 0-based 주소와 1-based register 번호 중 무엇을 요구하는지 확인하지 않으면 정확히 한 칸 어긋난 레지스터를 쓰게 된다.

프레임 생성 함수 modbus_send_make가 FC03·FC06·FC16 요청을 만들고 flange_serial_write가 바이트를 전송하더라도, 쓰기 호출 성공만으로 동작 성공을 판단해서는 안 된다. 응답의 장치 주소와 function code, exception 응답, byte count, CRC를 확인하고 timeout과 재시도 횟수를 제한한다. gripper_move(stroke)처럼 스트로크 값을 받는 래퍼에서는 입력이 mm인지 장치 내부 count인지 이름과 주석으로 명확히 하고, 물리 범위를 clamp한 뒤 전송한다. 예제의 300 같은 값은 300 mm가 아니라 장치 명령 단위일 수 있으므로 사양의 106 mm와 직접 비교하면 안 된다.

처음 연결할 때는 로봇을 정지시키고 그리퍼를 무부하 상태로 둔 뒤, 장치 주소와 통신 설정을 읽어 확인한다. 낮은 전류로 torque enable, 작은 위치 변화, open/close 순서로 시험하고, 응답이 없을 때 같은 명령을 무한 재전송하지 않는다. 비상정지와 보호정지의 동작도 공정 프로그램보다 먼저 검증한다.

42.11 미니프로젝트: 경로 쓰기와 블록 쌓기#

하트 그리기나 팀 이름 쓰기는 단순 시연처럼 보이지만 TCP 좌표계, 경로 연속성, 속도, 원호와 블렌딩을 함께 검증하는 좋은 과제다. 먼저 작업면에 수직인 접근 pose를 만들고, 충분히 떨어진 안전 높이에서 시작한다. 공구가 작업면에 닿기 전에는 블렌딩을 끄거나 줄이고, 그리기 구간에서만 필요한 연속성을 준다. 좌우나 상하가 뒤집히면 waypoint 값을 임의로 고치기 전에 base, tool, user frame 중 어떤 좌표계를 썼는지 확인한다.

블록 쌓기는 접근 → 그리퍼 열기 → 하강 → 파지 → 상승 → 이동 → 하강 → 놓기 → 후퇴의 상태 기계로 설계한다. 파지와 놓기 직전에는 직선 MoveL을 사용하고, 넓은 자유공간 이동은 MoveJ로 줄일 수 있다. 접촉 구간에 큰 블렌딩을 주면 파지점이나 놓기점을 건너뛸 수 있으므로 정지점을 명확히 한다. 그리퍼 명령 뒤에는 고정 대기시간만 두기보다 가능하면 위치·전류·완료 상태를 확인하고, 실패하면 안전 높이로 복귀하도록 한다.

팀 자유 과제는 ‘모션이 된다’보다 재실행 가능성을 기준으로 평가한다. 시작 자세, TCP와 payload, 기준 좌표계, 속도·가속도, 그리퍼 초기화, 실패 복구, 종료 자세를 프로그램 안에 명시하고, 공정의 각 단계가 독립적으로 시험 가능해야 한다. 최종 실행 전에는 저속 dry run, 무부하 gripper test, 단일 cycle, 반복 cycle 순으로 검증한다.

장 핵심 정리#

  • MoveIt2는 상위 motion planning framework이며 실제 servo safety layer가 아니다.

  • SRDF는 planning 의미를 추가한다.

  • Planning Scene에 환경을 넣어야 collision-aware planning이 가능하다.

  • E0509 공식 ROS2 stack의 distro 지원 범위를 학습용 Jazzy 환경과 구분한다.

  • DART Task Writer는 컨트롤러 로컬 작업 프로그램이고 MoveIt2는 외부의 상위 경로 계획 계층이다.

  • 서보 온은 동작 가능 상태를 만들 뿐 안전을 보장하지 않으며, 핸드가이딩과 조그는 저속에서 상태·좌표계·작업 공간을 확인한 뒤 사용한다.

  • MoveJ와 MoveL은 같은 목표라도 중간 경로가 다르며, 상대 이동은 기준 좌표계에 따라 방향이 달라진다.

  • RS-485는 전기 계층이고 Modbus RTU는 프레임 규칙이며, register 표기의 0-based/1-based 차이를 확인해야 한다.

확인 문제#

  1. URDF와 SRDF의 역할 차이를 설명하라.

  2. Planning Scene에 작업대를 넣지 않으면 어떤 문제가 생길 수 있는가?

  3. MoveIt2 collision checking이 산업용 안전기능을 대체하지 못하는 이유를 설명하라.

  4. MoveJ와 MoveL의 경로 차이와 각각 적합한 작업 구간을 설명하라.

  5. base frame 상대 이동과 tool frame 상대 이동이 다르게 보이는 이유를 설명하라.

  6. ZYZ 오일러 각의 회전 순서와 로봇팔 특이점이 MoveL 실행에 미치는 영향을 설명하라.

  7. Modbus 문서의 40257과 PDU 주소 256이 같은 register를 가리킬 수 있는 이유를 설명하라.

  8. 블록 쌓기에서 파지·놓기 직전에 블렌딩을 줄여야 하는 이유를 설명하라.

43장. Hand-Eye·3D 비전·Pick 파이프라인#

이 장을 끝내면 camera 3D point를 robot base 좌표로 변환해 Pick pipeline을 설계한다.

43.1 2D 검출만으로 로봇을 움직일 수 없는 이유#

YOLO bounding box 중심 `(u,v)`는 영상 평면의 픽셀 좌표일 뿐 3차원 위치가 아니다. Depth·카메라 내부파라미터(intrinsic) 또는 별도의 기하학적 복원으로 camera frame의 3D point를 얻은 뒤, Hand–Eye calibration에서 구한 외부변환(extrinsic)을 TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)로 적용해 base frame의 목표 위치로 바꾼다. 로봇이 파지해야 한다면 물체의 방향(orientation)도 추정해야 한다.

그림 43-1. 비전 결과가 실제 로봇 motion으로 변환되는 대표 pipeline.

43.2 Pixel + Depth → Camera 3D#

Pinhole camera model에서 depth `Z`와 intrinsic `(f_x,f_y,c_x,c_y)`가 있으면 대략 다음처럼 camera frame 3D point를 계산할 수 있다.

X=(u−cx)Zfx,Y=(v−cy)Zfy,Z=depth(u,v) X=\frac{(u-c_x)Z}{f_x},\qquad Y=\frac{(v-c_y)Z}{f_y},\qquad Z=\operatorname{depth}(u,v)
where
u, v영상 속 픽셀의 열·행 좌표 [pixel]
fₓ, fᵧ카메라 초점거리의 픽셀 단위 표현
cₓ, cᵧ주점(principal point) 좌표 [pixel]
X, Y, Z카메라 좌표계의 3차원 위치 [m]
depth(u,v)해당 픽셀에서 유효한 깊이 측정 [m]

실제 depth camera에서는 왜곡 보정(distortion), 컬러·깊이 픽셀 정렬(alignment), 유효하지 않은 픽셀, depth scale, optical frame 축 규약을 확인한다. 특히 대상 물체의 반사나 결측이 심하면 bounding box 중심 한 픽셀보다 관심 영역의 유효 depth를 여러 개 사용하는 편이 안정적이다.

43.3 Camera 3D → Robot Base#

카메라 좌표를 얻은 뒤에는 `T_base_camera`와 곱해 robot base 좌표로 바꾼다. 이 단계가 실패하면 비전 정확도와 무관하게 robot이 잘못된 곳으로 움직인다. 항상 알려진 calibration target으로 좌표 변환만 따로 검증한다.

base𝐩=baseTcameracamera𝐩 {}^{\mathrm{base}}\mathbf p={}^\mathrm{base}T_{\mathrm{camera}}\,{}^\mathrm{camera}\mathbf p
where
p점의 동차좌표
base / camera로봇 본체 / 카메라의 좌표계
Tcamera 좌표를 base 좌표로 옮기는 TF2 기반 강체변환

이 한 줄이 성립하려면 `T_base_camera`가 올바르게 calibration되어 있어야 하고, object point와 transform이 같은 시각에 대응해야 한다. Eye-in-hand에서 로봇팔이 움직이는 동안 오래된 camera transform을 쓰면 위치 오차가 생긴다.

43.4 Orientation까지 필요할 때#

단순 point pick은 위치만으로 충분할 수 있지만 suction/gripper 접근각이 중요하면 object orientation과 grasp frame이 필요하다. 이 경우 6D pose estimation, surface normal, fixture 기준축 등을 고려한다.

단순 point pick이면 위치만으로 충분해 보이지만 gripper 접근 방향, 물체 회전, 표면 normal이 중요할 수 있다. Object pose를 추정한 뒤 gripper target pose를 별도로 설계한다. 검출 object frame과 실제 grasp frame을 같은 것으로 가정하지 않는다.

43.5 Pick workflow#

실제 Pick은 detect→3D→TF(Transform, 좌표변환)→pre-grasp→approach→grasp→retreat→place의 상태기계로 분리하는 것이 좋다. 각 단계가 독립적으로 검증 가능해야 비전 오류와 motion 오류를 구분할 수 있다.

  1. Perception node가 object candidate와 confidence/pose를 발행

  2. Task node가 목표 object를 선택

  3. TF2로 camera/object pose를 robot base로 변환

  4. 접근 pose(pre-grasp) 생성

  5. Planning Scene에 장애물/물체 반영

  6. MoveIt2로 pre-grasp plan/execute

  7. 직선 또는 제어된 approach

  8. Gripper close 및 grasp 확인

  9. retreat 및 place pose 이동

  10. 실패 시 인식 재시도/재계획

43.6 Error budget를 생각한다#

Pick 위치 오차는 detector의 pixel 오차만이 아니라 camera intrinsic, depth 노이즈, Hand-Eye 외부파라미터, robot kinematic calibration과 repeatability, gripper TCP(Tool Center Point, 공구 중심점), object pose 추정 오차가 누적된 결과다. 끝점 오차만 보고 한 파라미터를 임의로 보정하기보다 카메라 좌표·로봇 기준 좌표·그리퍼 접촉 위치의 오차를 단계별로 분리 측정하여 개선 우선순위를 정한다.

43.7 자연어 → 구조화 명령 → ROS 2(Robot Operating System 2, 로봇 운영체제 2)#

로봇에 LLM(Large Language Model, 대규모 언어 모델)을 연결할 때는 “빨간 컵을 왼쪽으로 옮겨” 같은 문장을 직접 joint command로 바꾸지 않는다. 먼저 `object=red_cup`, `destination=left_bin`, `action=pick_place`처럼 허용 범위가 제한된 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 스키마로 변환한다. Validator가 대상·좌표·동작 권한을 확인한 뒤 결정론적 task node가 인식과 MoveIt2 Action을 실행하도록 분리하면, 해석 오류를 추적하고 동작을 시험하기 쉽다.

장 핵심 정리#

  • 비전 pixel은 robot 좌표가 아니다. Camera model과 TF가 연결되어야 한다.

  • Object pose와 grasp pose를 구분한다.

  • Pick 정확도는 여러 calibration/noise source가 합쳐진 error budget다.

  • LLM은 high-level intent를 구조화하고 실제 motion은 검증 가능한 ROS(Robot Operating System, 로봇 운영체제) task layer가 수행하는 편이 좋다.

확인 문제#

  1. pixel `(u,v)`와 depth에서 camera 3D point를 계산하는 식을 설명하라.

  2. Eye-in-hand에서 timestamp가 중요한 이유를 설명하라.

  3. detector 오차와 Hand-Eye 오차를 구분해 측정할 실험을 제안하라.

Part IX. 재사용 가능한 ROS 2 시스템#

44장. 실습 프로젝트 — Custom Interface + Lifecycle Node#

44.1 문자열이 아니라 시스템의 의미를 타입으로 만든다#

비전 결과를 `"cup,0.2,0.1,0.8"` 같은 String으로 보내면 빠르게 데모는 만들 수 있지만 frame, timestamp, 필드 타입, 자동 검증을 잃는다. `TrackedTarget.msg`에 Header와 의미 있는 필드를 정의해 같은 데이터 구조를 Python/rosbag2/RViz 도구가 공유하게 만든다.

std_msgs/Header header
string label
float32 x
float32 y
float32 z
float32 confidence

44.2 interface package의 핵심 파일#

cmake_minimum_required(VERSION 3.8)
project(r2lab_interfaces)

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/TrackedTarget.msg"
  "srv/SetMode.srv"
  "action/TimedMove.action"
  DEPENDENCIES std_msgs
)

ament_export_dependencies(rosidl_default_runtime)
ament_package()

CMake/rosidl 해설 CMakeLists.txt는 C++ 코드만 위한 파일이 아니다. ROS interface package에서는 find_package(rosidl_default_generators)와 rosidl_generate_interfaces()가 .msg/.srv/.action으로부터 언어별 타입 코드를 생성하도록 지시한다. 마지막 ament_package()가 package를 ament 빌드 시스템에 마감 등록한다.

<?xml version="1.0"?>
<package format="3">
  <name>r2lab_interfaces</name>
  <version>0.1.0</version>
  <description>Custom messages, services, and actions for the ROS 2 practical textbook</description>
  <maintainer email="student@example.com">student</maintainer>
  <license>Apache-2.0</license>
  <buildtool_depend>ament_cmake</buildtool_depend>
  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <depend>std_msgs</depend>
  <member_of_group>rosidl_interface_packages</member_of_group>
  <export><build_type>ament_cmake</build_type></export>
</package>
cd ~/ros2_course_ws
colcon build --packages-select r2lab_interfaces
source install/setup.bash
ros2 interface show r2lab_interfaces/msg/TrackedTarget

Interface 생성 해설 ros2 interface show가 custom message를 보여준다는 것은 rosidl code generation과 install이 완료되어 현재 overlay에서 해당 타입을 찾을 수 있다는 뜻이다. .msg 파일을 수정했으면 생성 코드를 다시 만들기 위해 재빌드가 필요하다.

44.3 LifecycleNode 전체 구현#

from typing import Optional

import rclpy
from rclpy.executors import SingleThreadedExecutor
from rclpy.lifecycle import LifecycleNode, State, TransitionCallbackReturn
from rclpy.lifecycle.publisher import LifecyclePublisher
from rclpy.timer import Timer

from r2lab_interfaces.msg import TrackedTarget


class LifecycleTarget(LifecycleNode):
    def __init__(self):
        super().__init__('lifecycle_target')
        self._pub: Optional[LifecyclePublisher] = None
        self._timer: Optional[Timer] = None
        self._x = 0.0

    def on_configure(self, state: State):
        self._pub = self.create_lifecycle_publisher(TrackedTarget, 'tracked_target', 10)
        self._timer = self.create_timer(0.2, self.tick)
        self.get_logger().info('configured: resources created')
        return TransitionCallbackReturn.SUCCESS

    def on_activate(self, state: State):
        self.get_logger().info('activated: publishing enabled')
        return super().on_activate(state)

    def on_deactivate(self, state: State):
        self.get_logger().info('deactivated: publishing disabled')
        return super().on_deactivate(state)

    def on_cleanup(self, state: State):
        if self._timer is not None:
            self.destroy_timer(self._timer)
            self._timer = None
        if self._pub is not None:
            self.destroy_publisher(self._pub)
            self._pub = None
        self.get_logger().info('cleaned up')
        return TransitionCallbackReturn.SUCCESS

    def tick(self):
        if self._pub is None:
            return
        msg = TrackedTarget()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = 'camera_link'
        msg.label = 'demo_target'
        msg.x = self._x
        msg.y = 0.15
        msg.z = 0.8
        msg.confidence = 0.9
        self._x += 0.01
        self._pub.publish(msg)


def main(args=None):
    rclpy.init(args=args)
    node = LifecycleTarget()
    executor = SingleThreadedExecutor()
    executor.add_node(node)
    try:
        executor.spin()
    except KeyboardInterrupt:
        pass
    finally:
        executor.shutdown()
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Lifecycle/Executor 해설 SingleThreadedExecutor는 준비된 callback을 한 스레드에서 순차 실행한다. LifecycleNode는 일반 Node에 configure/activate/deactivate/cleanup 같은 명시적 상태전이를 더한다. TransitionCallbackReturn은 각 전이 callback이 성공·실패했는지 lifecycle state machine에 돌려주는 값이다.

44.4 상태를 직접 전환한다#

# 터미널 A
ros2 run r2lab_nodes lifecycle_target

# 터미널 B
ros2 lifecycle get /lifecycle_target
ros2 lifecycle set /lifecycle_target configure
ros2 lifecycle get /lifecycle_target
ros2 topic echo /tracked_target

# 아직 inactive면 managed publisher가 실제 publish하지 않는다.
ros2 lifecycle set /lifecycle_target activate
ros2 lifecycle get /lifecycle_target
# echo에 메시지가 나타나는지 확인

ros2 lifecycle set /lifecycle_target deactivate
ros2 lifecycle set /lifecycle_target cleanup

Lifecycle의 핵심은 “프로세스가 떠 있다”와 “외부 기능을 수행할 준비가 됐다”를 분리하는 것이다. Nav2가 여러 server를 configure한 뒤 activate하는 이유도 같은 문제를 시스템 수준에서 해결하기 위해서다.

44.5 의도적으로 고장내기#

  • configure하지 않고 activate 요청을 보내 transition 결과를 확인한다.

  • inactive 상태에서 topic 이름은 보이지만 실제 샘플이 발행되지 않는 이유를 설명한다.

  • cleanup 후 다시 configure→activate하여 리소스가 재생성되는 것을 확인한다.

45장. 남이 실행할 수 있는 ROS 2 패키지로 마감하기#

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

45.1 “내 PC(Personal Computer, 개인용 컴퓨터)에서 된다”와 “ROS 2 패키지다”의 차이#

재현 가능한 패키지는 source code만 갖고 있지 않는다. 의존성이 package.xml에 기록되어 있고, executable/data가 install되며, parameter/launch/BT 파일의 위치를 package share에서 찾을 수 있고, 테스트가 있고, README에 실행 순서가 있다. 이 장은 작은 프로젝트를 전달 가능한 소프트웨어로 마감하는 절차다.

45.2 setup.py에서 설치 데이터까지 읽는다#

from glob import glob
import os
from setuptools import find_packages, setup

package_name = 'r2lab_nodes'

setup(
    name=package_name,
    version='0.1.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages', ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
        (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
        (os.path.join('share', package_name, 'behavior_trees'), glob('behavior_trees/*.xml')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='student',
    maintainer_email='student@example.com',
    description='Hands-on examples for the ROS 2 practical textbook',
    license='Apache-2.0',
    entry_points={
        'console_scripts': [
            'heartbeat = r2lab_nodes.heartbeat:main',
            'number_pub = r2lab_nodes.number_pub:main',
            'number_sub = r2lab_nodes.number_sub:main',
            'bump_go = r2lab_nodes.bump_go:main',
            'tf_obstacle_detector = r2lab_nodes.tf_obstacle_detector:main',
            'vff_avoidance = r2lab_nodes.vff_avoidance:main',
            'lifecycle_target = r2lab_nodes.lifecycle_target:main',
            'nav_to_pose = r2lab_nodes.nav_to_pose:main',
            'patrol = r2lab_nodes.patrol:main',
            'executor_demo = r2lab_nodes.executor_demo:main',
            'timed_move_client = r2lab_nodes.timed_move_client:main',
            'timed_move_server = r2lab_nodes.timed_move_server:main',
            'mode_client = r2lab_nodes.mode_client:main',
            'mode_server = r2lab_nodes.mode_server:main',
        ],
    },
)

setup.py 해설 setup.py는 Python 문법으로 작성하지만 목적은 package 설치 규칙 선언이다. find_packages()는 설치할 Python package를 찾고, data_files는 launch/config 같은 비-Python 파일도 install 공간에 복사한다. console_scripts entry point가 있어야 ros2 run <package> <executable> 이름으로 실행할 수 있다.

`data_files`에 launch/config/behavior_trees를 넣는 이유는 `colcon build` 뒤 install 공간에서도 패키지가 완결되어야 하기 때문이다. 소스 폴더의 상대경로를 우연히 참조하는 코드는 다른 PC나 binary install에서 깨지기 쉽다.

45.3 package.xml은 “이 프로그램을 재현하기 위한 계약”이다#

<?xml version="1.0"?>
<package format="3">
  <name>r2lab_nodes</name>
  <version>0.1.0</version>
  <description>Hands-on examples for ROS 2 Jazzy training</description>
  <maintainer email="student@example.com">student</maintainer>
  <license>Apache-2.0</license>
  <buildtool_depend>ament_python</buildtool_depend>
  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>sensor_msgs</exec_depend>
  <exec_depend>geometry_msgs</exec_depend>
  <exec_depend>visualization_msgs</exec_depend>
  <exec_depend>tf2_ros</exec_depend>
  <exec_depend>tf2_geometry_msgs</exec_depend>
  <exec_depend>nav2_simple_commander</exec_depend>
  <exec_depend>r2lab_interfaces</exec_depend>
  <test_depend>ament_pytest</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <export><build_type>ament_python</build_type></export>
</package>

Python에서 import가 성공했다고 package.xml dependency가 필요 없는 것은 아니다. 로컬 PC에 우연히 설치되어 있었던 패키지가 새 PC에는 없을 수 있다. `rosdep`가 dependency를 설치할 수 있도록 실제 runtime 의존성을 선언한다.

45.4 전체 workspace를 깨끗한 상태에서 다시 빌드#

cd ~/ros2_course_ws
rm -rf build install log
source /opt/ros/jazzy/setup.bash
rosdep install --from-paths src --ignore-src -r -y
colcon build --symlink-install
source install/setup.bash
colcon test
colcon test-result --verbose

clean build 해설 rm -rf는 지정 디렉터리를 확인 없이 재귀 삭제하는 강한 명령이다. 여기서는 build/install/log가 재생성 가능한 산출물이라는 전제에서만 사용한다. 개발 중 오류가 날 때마다 습관적으로 전체 삭제하지 말고, release 검증이나 캐시 오염이 확실할 때 제한적으로 사용한다.

평소에는 build/install 전체 삭제를 습관처럼 하지 않지만, “새 PC에서도 되는가”를 확인하는 release candidate 단계에서는 clean build가 의미 있다. 기존 install 잔재가 누락된 dependency나 설치 규칙 오류를 가려줄 수 있기 때문이다.

45.5 최소 README가 답해야 할 질문#

  • 어떤 ROS 2 배포판/Ubuntu에서 검증했는가

  • 어떤 시스템 패키지/ROS(Robot Operating System, 로봇 운영체제) dependency가 필요한가

  • workspace 어디에 clone/복사하는가

  • 빌드 명령은 무엇인가

  • 대표 launch/run 명령은 무엇인가

  • 입력/출력 Topic·Service·Action은 무엇인가

  • 필수 parameter와 기본값은 무엇인가

  • 시뮬레이터와 실제 로봇에서 다른 점은 무엇인가

  • 실패했을 때 첫 진단 명령은 무엇인가

45.6 Git에 올릴 때#

cd ~/ros2_course_ws
printf "build/\ninstall/\nlog/\n__pycache__/\n*.pyc\n" >> .gitignore
git init
git add src README.md .gitignore
git status

shell redirection 해설 printf는 문자열을 출력하고, >>는 그 출력을 파일 끝에 append한다. >는 파일을 덮어쓰므로 차이를 구분한다. __pycache__/와 *.pyc는 Python이 만드는 bytecode cache이고 ROS 소스와 함께 버전관리할 필요가 없다.

Git 해설 git init은 현재 디렉터리를 저장소로 만들고, git add는 다음 commit에 포함할 변경을 staging area에 올린다. .gitignore는 build/install/log처럼 재생성 가능한 파일을 버전관리에서 제외한다. ROS workspace는 소스와 metadata만으로 새 PC에서 재구성 가능해야 한다.

build/install/log는 산출물이므로 보통 소스 저장소에 넣지 않는다. 대신 source + dependency metadata + launch/config + tests만으로 새 workspace를 재구성할 수 있어야 한다.

45.7 v4 실습 트랙 최종 검증#

  • 내가 만든 Node를 package/entry point 구조로 설명하고 직접 추가할 수 있다.

  • Topic/Service/Action 중 어떤 인터페이스가 맞는지 선택하고 Python server/client를 만들 수 있다.

  • Launch/parameter/remapping으로 같은 코드를 다른 Graph에 재사용할 수 있다.

  • LaserScan QoS(Quality of Service, 서비스 품질)와 메시지 구조를 확인하고 callback에서 실제 로봇 행동을 만들 수 있다.

  • TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)로 sensor frame의 Point/Pose를 odom/map/base frame으로 변환하고 timestamp 오류를 진단할 수 있다.

  • rosbag2로 센서 입력을 기록·재생해 알고리즘을 하드웨어에서 분리할 수 있다.

  • Lifecycle 상태를 직접 전환하고 Active 여부와 process 존재를 구분할 수 있다.

  • Nav2를 action/lifecycle/costmap/planner/controller/BT(Behavior Tree, 행동 트리) 구조로 관찰하고 Python에서 목표를 보낼 수 있다.

  • SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) Toolbox로 map을 만들고 저장한 뒤 AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정) 기반 localization 흐름과 map→odom 역할을 설명할 수 있다.

  • Executor/Callback Group/QoS/Time 문제를 의도적으로 재현하고 CLI(Command-Line Interface, 명령줄 인터페이스)로 원인을 좁힐 수 있다.

여기까지 실제로 완료했다면 ROS 2를 “개념적으로 안다”가 아니라, 작은 ROS 2 시스템을 만들고 관찰하고 고칠 수 있는 상태다. 이후 E0509/MoveIt2/비전 통합에서도 새 API를 외우기보다 같은 Graph·Frame·State·Interface 관점으로 driver와 capability를 읽으면 된다.

Part X. 전문가 ROS 2 엔지니어링#

46장. Executor·Callback Group·시간·동시성#

이 장을 끝내면 Executor와 Callback Group의 설정을 바꾸어 응답 지연·starvation을 직접 측정하고, QoS(Quality of Service, 서비스 품질) 불일치와 시간 동기화 오류를 재현해 원인을 분리한다.

46.1 spin은 “대기”가 아니라 callback 스케줄링이다#

`rclpy.spin(node)`은 CPU를 무한 루프로 점유하는 단순 반복문이 아니다. middleware에서 준비된 이벤트를 기다리고, 실행 가능한 callback을 executor가 선택해 호출하는 구조다. Subscriber, Timer, Service, Action callback은 모두 이 스케줄링 대상이다.

46.2 SingleThreadedExecutor#

SingleThreadedExecutor에서는 한 시점에 하나의 callback만 실행된다. 구조가 단순하고 공유 상태 race condition이 적지만, 하나의 callback이 오래 blocking되면 다른 callback이 모두 지연된다. 예를 들어 subscriber callback에서 3초짜리 inference를 동기 실행하면 timer와 service response까지 밀릴 수 있다.

46.3 MultiThreadedExecutor와 Callback Group#

MultiThreadedExecutor는 여러 callback을 병렬로 실행할 수 있지만 “스레드가 많으면 빨라진다”로 끝나지 않는다. callback group이 MutuallyExclusive이면 같은 그룹의 callback은 동시에 실행되지 않는다. Reentrant 그룹은 동시 실행을 허용한다. 공유 데이터 보호와 라이브러리 thread-safety를 고려해야 한다.

구성 장점 위험
SingleThreaded 단순, 예측 쉬움 blocking callback이 전체 지연
MultiThreaded + MutuallyExclusive 일부 병렬성, 그룹 내 직렬화 그룹 배치가 나쁘면 기대만큼 병렬화 안 됨
MultiThreaded + Reentrant 높은 동시성 race condition, 재진입 안전성 필요

46.4 ROS 2(Robot Operating System 2, 로봇 운영체제 2)에서 자주 생기는 deadlock#

한 callback 안에서 다른 Service를 동기 호출하고 결과가 올 때까지 기다리는데, 그 response callback이 같은 MutuallyExclusive group에서 실행되어야 한다면 서로 영원히 기다릴 수 있다. 해결은 async API 사용, callback group 분리, executor 구조 수정 등이다. “callback 안에서 오래 기다리지 않는다”는 원칙이 중요하다.

설계 규칙 Callback은 가능한 짧게 유지한다. 오래 걸리는 계산은 worker thread/process, action server, queue 기반 pipeline 등으로 분리하고 callback은 입력을 받아 상태를 갱신하거나 작업을 enqueue하는 역할에 가깝게 만든다.

기본 Callback Group 확인: 별도로 그룹을 지정하지 않은 Timer·Subscription·Service Client는 Node의 기본 MutuallyExclusive 그룹에 들어간다. 따라서 MultiThreadedExecutor를 사용하더라도 같은 그룹의 Callback은 동시에 실행되지 않는다.

Service Client가 만든 응답 처리와 Future 완료 Callback도 관련 Client의 Callback Group 제약을 받는다. Callback 내부에서 동일 그룹의 응답을 동기적으로 기다리면 교착상태가 발생할 수 있으므로 call_async()로 요청하고 응답은 후속 Callback에서 처리한다.

46.5 ROS(Robot Operating System, 로봇 운영체제) Time, system time, simulated time#

로봇 데이터에는 시간축이 있다. 센서 timestamp가 다르면 같은 순간의 자세와 센서값을 맞출 수 없다. ROS 2는 system clock뿐 아니라 `/clock` 기반 simulated time을 지원한다. Gazebo나 bag 재생에서 `use_sim_time`이 중요한 이유다.

ros2 param get /my_node use_sim_time
ros2 param set /my_node use_sim_time true

TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)가 “과거/미래로 extrapolation할 수 없다”고 오류를 내는 문제도 결국 timestamp와 buffer에 저장된 transform 시간 범위가 맞지 않는 것이다. 이는 26장의 TF2 절에서 다룬 frame과 시간 일치 문제다.

46.6 Node를 어떻게 나눌 것인가#

  • 하나의 명확한 책임을 가진다. 센서 driver와 task planning을 같은 Node에 넣지 않는다.

  • 고빈도 데이터 경로와 저빈도 관리 기능을 분리한다.

  • 실패를 격리하고 독립 재시작할 가치가 있는 기능은 별도 Node가 유리하다.

  • 반대로 아주 작은 변환 노드를 지나치게 많이 만들어 serialization/복잡성을 키우지 않는다.

  • 성능이 중요한 경로는 composition/intra-process communication을 고려한다.

장 핵심 정리#

  • Executor는 callback을 언제 실행할지 결정한다.

  • SingleThreaded에서는 하나의 blocking callback이 전체 노드를 막을 수 있다.

  • MultiThreaded를 쓰면 callback group과 공유상태를 함께 설계해야 한다.

  • ROS 시스템에서 timestamp는 단순 기록값이 아니라 센서융합·TF(Transform, 좌표변환)의 핵심 축이다.

확인 문제#

  1. callback 안에서 동기 Service 호출이 deadlock을 만들 수 있는 상황을 설명하라.

  2. MultiThreadedExecutor가 항상 더 빠르지 않은 이유를 설명하라.

  3. use_sim_time이 필요한 두 가지 상황을 들어라.

47장. rosbag2·로그·네트워크·WSL2·성능 진단#

이 장을 끝내면 현장 데이터를 반복 가능한 디버깅 fixture로 만들고 네트워크/지연 문제를 분리한다.

약어 표기 WSL2: Windows Subsystem for Linux 2(Windows용 Linux 하위 시스템 2)

47.1 rosbag2는 녹화가 아니라 재현 장치다#

rosbag2의 가장 큰 가치는 현실에서 한 번 발생한 상황을 반복 가능한 입력으로 바꾸는 것이다. 센서/TF(Transform, 좌표변환)/odom을 함께 기록해 두면 SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성), perception, timing 문제를 장비 없이 반복 디버깅할 수 있다.

현장에서 한 번 발생한 문제를 매번 실제 로봇으로 재현하려 하지 않는다. 관련 Topic을 rosbag2로 기록하면 동일한 sensor/state 입력을 반복 재생하면서 perception, SLAM, 상위 logic을 독립적으로 디버깅할 수 있다. 이는 robotics에서 매우 강력한 실험 재현성 도구다.

ros2 bag record /scan /odom /tf /tf_static /imu/data
ros2 bag info <bag_dir>
ros2 bag play <bag_dir> --clock

Simulation time 해설 --clock은 bag의 기록 시간에 맞춰 /clock Topic도 발행한다. 이를 사용하는 Node는 use_sim_time=true여야 같은 시간축에서 TF와 sensor timestamp를 해석한다. wall clock과 simulation clock을 섞으면 TF extrapolation이나 timeout이 비정상적으로 보일 수 있다.

bag 재생에서 `/clock`을 사용할 경우 분석 노드의 `use_sim_time` 설정을 맞춰야 한다. TF와 sensor timestamp를 함께 기록하지 않으면 재생 시 변환을 재현하지 못할 수 있다.

47.2 디버깅 계층#

문제를 만났을 때 코드부터 고치지 않는다. Process/Lifecycle → Graph/이름 → Type/QoS(Quality of Service, 서비스 품질) → 데이터값 → TF/time → algorithm parameter → 코드 순으로 내려가면 변경 범위를 줄일 수 있다.

  1. 프로세스: 실행/크래시 여부

  2. Graph: Node/Topic/Service/Action 존재

  3. Interface: 타입·QoS·namespace

  4. Data: 실제 값·rate·timestamp

  5. Frame: TF chain과 orientation

  6. State estimation: odom/covariance/localization

  7. Planner/controller: 경로와 command

  8. Hardware/network: IP(Internet Protocol, 인터넷 프로토콜), firewall, driver, safety

47.3 CLI(Command-Line Interface, 명령줄 인터페이스) 관찰 도구#

`node info`, `topic info -v`, `topic hz`, `topic delay`, `param dump`, `tf2_echo`, `view_frames`, `ros2 doctor`를 조합하면 GUI 없이도 시스템 대부분을 관찰할 수 있다. 전문가일수록 관찰 가능한 시스템을 먼저 만든다.

ros2 node info /node
ros2 topic info /topic -v
ros2 topic hz /topic
ros2 topic bw /topic
ros2 param dump /node
ros2 doctor --report
rqt_graph

Parameter dump 해설 ros2 param dump는 현재 Node의 parameter 값을 YAML로 저장할 수 있는 형태로 출력한다. 실험 중 바꾼 값을 재현 가능한 설정파일로 남길 때 유용하다. 다만 모든 parameter가 startup에서 다시 적용 가능한지는 해당 Node의 선언과 lifecycle에 따라 확인해야 한다.

진단 도구 해설 ros2 doctor --report는 ROS 설치·네트워크·RMW·환경 설정의 기본 상태를 한 번에 보고한다. topic hz는 빈도, topic bw는 대역폭을 측정한다. 이 값들은 느리다는 느낌을 latency, throughput, update rate 같은 측정 가능한 항목으로 분해하는 데 쓴다.

47.4 Latency와 throughput을 구분한다#

throughput은 단위시간 처리량, latency는 한 샘플이 입력에서 출력까지 걸리는 시간이다. 30 fps를 유지하면서도 1초 늦은 영상을 처리할 수 있으므로 둘을 같은 지표로 보면 안 된다.

처리량이 충분해도 latency가 커질 수 있다. 예를 들어 camera가 30 fps이고 detector도 평균 30 fps를 처리하지만 순간적으로 밀려 queue에 오래된 frame이 쌓이면 “30 fps이지만 1초 늦은 결과”가 나올 수 있다. 로봇 제어에서는 throughput보다 end-to-end latency가 더 중요한 경우가 많다.

47.5 QoS depth와 stale data#

처리속도보다 입력속도가 빠르면 queue depth가 클수록 drop은 줄어도 오래된 데이터가 쌓일 수 있다. 로봇 제어에서는 과거 데이터를 모두 처리하는 것보다 최신 상태로 따라잡는 것이 더 중요한 경우가 많다.

queue depth를 크게 만들면 데이터 유실이 줄어드는 대신 뒤처진 consumer가 오래된 데이터를 처리할 수 있다. 제어/비전 pipeline에서는 “최신 하나”가 중요할 수 있으므로 history/depth를 task 의미에 맞게 정해야 한다.

47.6 DDS(Data Distribution Service, 데이터 분산 서비스) discovery와 ROS_DOMAIN_ID#

ROS_DOMAIN_ID는 같은 네트워크에서 discovery 도메인을 분리한다. 서로 다른 ID는 기본적으로 서로를 발견하지 않는다. 여러 팀이 같은 LAN에서 실습할 때 충돌을 막는 데 유용하지만 노드가 안 보임 문제의 원인이 되기도 한다.

같은 네트워크의 ROS 2 노드는 discovery를 통해 서로를 찾는다. 여러 팀/로봇이 같은 LAN을 쓰면 원치 않는 노드가 보일 수 있어 `ROS_DOMAIN_ID`로 논리적으로 분리할 수 있다. Domain ID가 다르면 같은 네트워크여도 기본적으로 서로 발견하지 않는다.

export ROS_DOMAIN_ID=42

환경변수 해설 export는 현재 shell과 그 자식 프로세스가 볼 환경변수를 설정한다. ROS_DOMAIN_ID는 DDS discovery domain을 분리하므로 서로 다른 값의 ROS 2 프로세스는 기본적으로 서로 발견하지 못한다. 터미널마다 값이 다를 수 있으므로 printenv ROS_DOMAIN_ID로 실제 값을 확인한다.

47.7 WSL2와 실제 장비#

실제 로봇이 같은 LAN에 있어도 Windows 방화벽, WSL networking mode, multicast 전달 때문에 discovery가 달라질 수 있다. 먼저 ping 같은 IP 연결, 다음 DDS discovery, 마지막 ROS Topic 통신 순으로 층을 분리한다.

WSL2의 mirrored networking은 LAN 장비와 multicast 사용을 NAT보다 편하게 만들 수 있지만, Windows Firewall/Hyper-V 방화벽, static subnet, 여러 NIC, VPN이 여전히 영향을 줄 수 있다. 문제를 “ROS가 안 된다”로 뭉개지 말고 Windows에서 장비 IP reachable → WSL에서 ping/route → ROS/DDS discovery 순으로 단계화한다.

# Windows 사용자 폴더 .wslconfig 예
[wsl2]
networkingMode=mirrored

# 적용
wsl --shutdown

47.8 USB(Universal Serial Bus, 범용 직렬 버스) 센서#

USB passthrough가 필요한 센서는 OS에서 device가 보이는지부터 확인한다. `/dev/video*`, serial device, RealSense SDK 등 하드웨어 레벨 확인이 끝난 뒤 ROS driver를 본다. ROS node 오류만 보고 USB 연결문제를 추측하지 않는다.

WSL2에서는 USB passthrough가 가능하지만 device driver/kernel module과 연결 유지 측면에서 네이티브 Ubuntu보다 추가 단계가 생긴다. RealSense나 특정 LiDAR가 반복적으로 불안정하면 알고리즘 문제가 아니라 host/USB 계층을 먼저 의심한다.

47.9 로그를 설계한다#

좋은 로그는 무슨 함수가 실행됨보다 상태전이, goal id, frame, timestamp, 입력/출력 요약, 실패코드를 남긴다. 나중에 rosbag과 로그를 시간축으로 맞춰야 원인분석이 가능하다.

좋은 로그는 “실패했다”가 아니라 입력 state와 decision 이유를 남긴다. 목표 pose, 현재 pose, planner result, controller status, timeout, retry count를 구조적으로 기록하면 나중에 bag과 맞춰 원인을 재현하기 쉽다.

장 핵심 정리#

  • rosbag2는 현실 입력을 반복 가능한 테스트 fixture로 바꾼다.

  • ROS(Robot Operating System, 로봇 운영체제) 문제는 프로세스→Graph→Interface→Data→Frame→상위 알고리즘 순으로 층을 나눠 진단한다.

  • 처리량과 latency는 다른 성능지표다.

  • WSL 문제는 Windows network/USB 계층과 ROS 계층을 분리한다.

확인 문제#

  1. 30 fps 처리율인데도 결과가 1초 늦을 수 있는 상황을 설명하라.

  2. rosbag에 TF와 clock 정보가 중요한 이유를 설명하라.

  3. WSL에서 Ethernet 로봇이 안 보일 때 점검 순서를 제시하라.

48장. 통합 설계 원칙과 종합 실습#

48.1 좋은 ROS 2(Robot Operating System 2, 로봇 운영체제 2) 시스템의 여섯 원칙#

좋은 시스템은 기능이 많아서가 아니라 계약이 명확하고 관찰 가능하며 재현 가능하다. Node 책임이 분리되고, frame/time이 명시되며, launch/YAML로 환경을 재현하고, rosbag/test로 실패를 반복할 수 있어야 한다.

  1. 인터페이스는 의미에 맞게 선택한다. 스트림/질의/장시간 작업을 구분한다.

  2. 모든 공간 데이터는 frame과 timestamp를 추적한다.

  3. 상태추정과 전역 보정 역할을 분리한다.

  4. Node 책임을 분리하되 지나친 미세분할을 피한다.

  5. 설정은 parameter/launch로 재현 가능하게 만든다.

  6. 현장 실패를 rosbag·로그·상태코드로 재현 가능하게 만든다.

48.2 종합 실습 A - ROS(Robot Operating System, 로봇 운영체제) Graph 해부#

목표: 임의의 ROS 2 데모 또는 로봇 stack을 실행하고 코드 없이 Graph를 문서화한다.

  1. `ros2 node list`로 node 목록 수집

  2. 각 node에 `ros2 node info` 실행

  3. 주요 Topic 타입과 QoS(Quality of Service, 서비스 품질) 확인

  4. Service/Action 목록 수집

  5. rqt_graph 스크린샷과 함께 데이터 흐름 설명

  6. “이 node가 죽으면 어떤 기능이 사라지는가” 분석

48.3 종합 실습 B - TF(Transform, 좌표변환) 오류 만들고 고치기#

  1. base_link→camera_link static transform을 의도적으로 틀리게 설정

  2. RViz에서 point/scan이 어떻게 어긋나는지 관찰

  3. translation과 rotation 오차를 분리

  4. tf2_echo로 실제 transform 확인

  5. 수정 후 동일 bag을 재생해 전후 비교

48.4 종합 실습 C - Odometry calibration#

  1. 정해진 직선 거리 주행 후 encoder odom 거리 비교

  2. 제자리 360° 회전 후 heading 오차 측정

  3. wheel radius scale과 track width가 어떤 오차를 만드는지 추론

  4. 파라미터 수정 후 반복

  5. 오차를 표로 기록하고 재현성 확인

48.5 종합 실습 D - SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) 지도 품질#

  1. 작은 폐회로 경로 주행

  2. 동일 구간을 양방향 통과

  3. loop closure 전/후 map 관찰

  4. 잘못된 odom scale을 일부러 넣고 지도 비교

  5. 정상값 복원 후 bag replay로 동일 입력 비교

48.6 종합 실습 E - Nav2 계층 분리#

  1. AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정)/localization만 검증

  2. global costmap만 관찰

  3. 목표점에서 global path 생성 확인

  4. controller를 활성화해 tracking 확인

  5. inflation/footprint를 한 번에 하나씩 변경

  6. 일부 obstacle을 추가해 replanning/recovery 관찰

48.7 종합 실습 F - 비전 Pick 설계#

실제 로봇 실행 전 paper design으로 다음 인터페이스를 정의한다.

단계 입력 출력 실패
Detection Image/Depth ObjectDetection 미검출/낮은 confidence
3D projection pixel+depth+intrinsic PointStamped(camera) invalid depth
TF transform PointStamped PoseStamped(base) TF unavailable
Planning target pose+scene trajectory IK/planning failure
Execution trajectory result controller/safety stop

48.8 최종 프로젝트를 설계할 때#

프로젝트 완성도를 기능 수로 평가하지 않는다. 요구사항→interface→state→failure mode→test evidence가 연결되어야 한다. 예를 들어 “자율주행 성공”이 아니라 “10회 goal 중 성공률, 평균 소요시간, localization loss 횟수, recovery 발생 원인”처럼 측정한다.

완성 기준 작동 영상 하나보다, 동일 조건을 다시 실행해 같은 결과를 얻고 실패를 설명할 수 있는 시스템이 더 완성도가 높다.

장 핵심 정리#

  • 전체 시스템을 Graph/Frame/State/Contract 관점으로 분해하면 복잡도가 줄어든다.

  • 실습은 “성공시키기”뿐 아니라 의도적으로 실패시켜 원인을 관찰해야 한다.

  • 프로젝트 결과는 재현 가능한 측정과 실패분석으로 남긴다.

49장. 실습 프로젝트 — Executor·Callback Group·QoS 실패 실험#

이 장을 끝내면 Executor·Callback Group 조합별 응답 지연과 교착 조건을 Python 실습으로 재현하고 로그·실험 결과를 비교한다.

약어 표기 QoS: Quality of Service(서비스 품질)

49.1 “노드가 살아 있는데 느리다”를 재현한다#

ROS 2(Robot Operating System 2, 로봇 운영체제 2)를 실제로 쓰기 시작하면 문법 오류보다 callback 지연, deadlock, QoS mismatch처럼 “정상 실행처럼 보이는 실패”가 더 어렵다. 이 장에서는 일부러 그런 실패를 만든다.

49.2 Executor 실험 코드#

import time

import rclpy
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
from rclpy.executors import MultiThreadedExecutor
from rclpy.node import Node


class ExecutorDemo(Node):
    """Compare callback starvation with callback-group choices."""

    def __init__(self):
        super().__init__('executor_demo')
        self.declare_parameter('reentrant', False)
        self.declare_parameter('block_seconds', 1.0)
        reentrant = bool(self.get_parameter('reentrant').value)
        self.block_seconds = float(self.get_parameter('block_seconds').value)

        group_cls = ReentrantCallbackGroup if reentrant else MutuallyExclusiveCallbackGroup
        self.group = group_cls()

        self.fast_count = 0
        self.slow_count = 0
        self.fast_timer = self.create_timer(0.20, self.fast_cb, callback_group=self.group)
        self.slow_timer = self.create_timer(1.00, self.slow_cb, callback_group=self.group)

        mode = 'Reentrant' if reentrant else 'MutuallyExclusive'
        self.get_logger().info(f'callback group: {mode}, block={self.block_seconds:.2f}s')

    def fast_cb(self):
        self.fast_count += 1
        now = self.get_clock().now().nanoseconds / 1e9
        self.get_logger().info(f'FAST #{self.fast_count:03d} t={now:.3f}')

    def slow_cb(self):
        self.slow_count += 1
        self.get_logger().warn(f'SLOW #{self.slow_count:03d} begin')
        time.sleep(self.block_seconds)
        self.get_logger().warn(f'SLOW #{self.slow_count:03d} end')


def main(args=None):
    rclpy.init(args=args)
    node = ExecutorDemo()
    executor = MultiThreadedExecutor(num_threads=2)
    executor.add_node(node)
    try:
        executor.spin()
    finally:
        executor.shutdown()
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Callback Group 해설 Callback Group은 callback을 어떤 동시성 규칙으로 실행할지 묶는 단위다. MutuallyExclusive는 같은 그룹의 callback을 동시에 실행하지 않고, Reentrant는 동시 실행을 허용한다. MultiThreadedExecutor를 사용해도 모든 entity가 같은 기본 MutuallyExclusive group에 있으면 기대한 병렬성이 나오지 않는다.

49.3 MutuallyExclusive에서 starvation 관찰#

ros2 run r2lab_nodes executor_demo --ros-args \
  -p reentrant:=false -p block_seconds:=1.0

FAST timer는 0.2초 주기지만 같은 MutuallyExclusive group의 SLOW callback이 `sleep(1.0)` 중이면 실행되지 못한다. MultiThreadedExecutor를 썼다는 사실만으로 병렬 실행이 보장되지 않는다는 점을 로그 timestamp 간격으로 확인한다.

49.4 Reentrant로 바꾼다#

ros2 run r2lab_nodes executor_demo --ros-args \
  -p reentrant:=true -p block_seconds:=1.0

두 thread가 있고 callback group이 Reentrant이므로 SLOW가 잠든 동안 FAST가 실행될 수 있다. 하지만 실제 코드에서 공유 mutable state가 있다면 race condition을 새로 만들 수 있다. “안 막힌다”와 “thread-safe다”는 별개다.

49.5 QoS mismatch를 진단하는 순서#

ros2 topic info /scan -v
ros2 topic echo /scan --qos-reliability best_effort
ros2 topic hz /scan

센서 endpoint가 BEST_EFFORT 제공인데 Subscriber가 RELIABLE을 요구하면 호환되지 않을 수 있다. 이름/타입이 같고 node도 살아 있는데 callback이 한 번도 안 오는 경우 QoS를 확인해야 한다. 반대로 모든 Topic을 BEST_EFFORT로 만들면 중요한 상태/명령을 잃어도 된다는 의미가 되어버린다. 데이터 의미에 맞춰 선택한다.

49.6 시간을 고장낸다#

bag replay나 simulator에서 일부 노드는 `use_sim_time=true`, 일부는 system time이면 timestamp와 TF buffer가 서로 다른 시계를 보게 된다. TF extrapolation 오류가 생기면 최신 transform을 강제로 쓰기 전에 모든 관련 노드의 `use_sim_time`과 `/clock`을 먼저 확인한다.

ros2 topic echo /clock --once
ros2 param get /<node_name> use_sim_time
ros2 run tf2_ros tf2_echo odom base_link

49.7 이 장의 진단 원칙#

증상 먼저 확인 그다음
callback이 뜸하게 실행 callback duration / executor / group CPU load, thread, queue
Topic 있는데 데이터 없음 type + QoS endpoint namespace/remap/network
TF lookup 실패 frame 존재 + timestamp publisher delay / clock
Service client가 영원히 기다림 server 존재 + executor 동기 호출 deadlock / callback group

50장. DDS/RMW·Discovery·QoS 요청/제공 모델#

전문가 목표 ROS 2(Robot Operating System 2, 로봇 운영체제 2) API(Application Programming Interface, 응용 프로그램 인터페이스) 아래에서 discovery와 QoS가 어떻게 연결을 만들거나 끊는지 설명하고, RMW를 바꿨을 때 무엇이 달라지는지 진단한다.

약어 표기 DDS: Data Distribution Service(데이터 분산 서비스) · RMW: ROS Middleware Interface(ROS 미들웨어 인터페이스) · QoS: Quality of Service(서비스 품질)

50.1 rclpy 아래의 계층을 실제 문제와 연결하기#

애플리케이션 코드는 rclpy/rclcpp를 사용하지만 실제 discovery와 data transport는 RMW 아래 미들웨어가 수행한다. 이 구조 덕분에 애플리케이션은 비교적 같은 API를 유지하면서 미들웨어 구현을 바꿀 수 있다. 반대로 discovery·QoS·네트워크 문제가 생기면 Python 코드만 읽어서는 원인이 보이지 않을 수 있다.

echo $RMW_IMPLEMENTATION
ros2 doctor --report
ros2 topic info /scan -v

RMW 해설 RMW는 rclpy/rclcpp와 실제 middleware 사이의 추상화 계층이다. RMW_IMPLEMENTATION 환경변수로 사용할 구현체를 선택할 수 있다. echo $RMW_IMPLEMENTATION이 비어 있어도 배포판의 기본 RMW가 선택될 수 있으므로 ros2 doctor와 실제 package 설치 상태를 함께 본다.

50.2 Discovery를 세 단계로 분리한다#

첫째 IP 계층에서 서로 통신 가능한가. 둘째 같은 ROS_DOMAIN_ID와 호환되는 discovery 설정을 쓰는가. 셋째 발견된 endpoint의 타입·QoS가 호환되는가. ping이 된다는 것은 첫 단계만 통과했다는 뜻이지 ROS 2 discovery 성공을 보장하지 않는다.

50.3 QoS는 Requested/Offered 계약이다#

Publisher는 자신이 제공할 수 있는 정책을 offer하고 Subscriber는 자신이 요구하는 정책을 request한다. Reliability와 Durability 같은 정책이 호환되지 않으면 endpoint가 서로 존재해도 데이터 연결이 성립하지 않을 수 있다. 센서 스트림과 상태 이벤트에 같은 QoS profile을 기계적으로 적용하지 않는다.

데이터 성격 우선 가치 전형적 선택 주의
고주파 센서 최신성·낮은 지연 BEST_EFFORT, 작은 depth 일부 loss 허용
중요 상태 이벤트 전달 안정성 RELIABLE queue 지연 감시
늦게 참가한 노드도 최신 설정 필요 마지막 값 유지 TRANSIENT_LOCAL 고려 양쪽 호환 필요

50.4 RMW를 바꿀 때 검증해야 할 것#

RMW 교체는 성능 버튼이 아니다. discovery 방식, multicast/network 환경, QoS 구현 특성, tooling, container/WSL 호환성을 다시 확인한다. 동일 bag과 동일 workload로 latency/CPU/discovery 안정성을 비교해야 의미가 있다.

50.5 전문가 실습#

  1. 두 터미널의 ROS_DOMAIN_ID를 다르게 설정해 노드가 사라지는 것을 확인한다.

  2. Publisher BEST_EFFORT / Subscriber RELIABLE 조합을 만들어 연결 실패를 관찰한다.

  3. `ros2 topic info -v`로 endpoint별 QoS를 기록하고 정상 조합으로 되돌린다.

51장. Composition·Intra-process·프로세스 경계 설계#

전문가 목표 노드를 언제 분리하고 언제 같은 process에 compose할지 성능·격리·재시작·배포 기준으로 판단한다.

51.1 Node 경계와 Process 경계는 같은 문제가 아니다#

Node는 ROS Graph의 논리적 책임 경계이고 Process는 OS 격리 경계다. 여러 Node를 하나의 process에 넣을 수도 있고 각 Node를 별도 process로 실행할 수도 있다. 논리적 모듈화와 운영 격리를 같은 기준으로 결정하면 과도한 process 분리 또는 거대한 monolith가 되기 쉽다.

51.2 Composition이 줄이는 비용#

같은 process 안에서 composable node를 사용하면 경우에 따라 message marshalling/unmarshalling과 process 간 전달 비용을 줄일 수 있다. 특히 큰 Image/PointCloud가 여러 단계를 통과하는 perception pipeline에서 중요해질 수 있다.

ros2 component types
ros2 run rclcpp_components component_container
ros2 component list

Composition CLI(Command-Line Interface, 명령줄 인터페이스) 해설 ROS 2 component는 별도 프로세스를 새로 띄우지 않고 이미 실행 중인 component container 안에 Node를 동적으로 로드할 수 있는 형태다. ros2 component types는 로드 가능한 component class를, ros2 component list는 container에 실제 올라간 component를 확인한다.

51.3 격리와 성능의 trade-off#

구성 장점 단점 적합한 상황
노드별 process 장애 격리, 독립 재시작 직렬화/IPC 비용, process 증가 드라이버·불안정 모듈
composed process 낮은 전달비용, 관리 단순화 한 crash 영향 큼 고속 perception pipeline
혼합 성능과 격리 균형 구성 복잡도 production robot

51.4 Python 사용자의 현실적 기준#

rclpy에서도 Node 분리 원칙은 동일하지만 C++의 component/intra-process 기능과 성능 특성이 더 풍부한 경우가 많다. 학습·상위 orchestration은 Python으로 빠르게 만들고, 고주파 영상/point cloud의 병목 구간은 측정 후 C++/composition을 검토하는 방식이 실용적이다.

52장. Tracing·Latency·Throughput·메모리·성능 측정#

전문가 목표 느린 것 같다가 아니라 callback 지연, end-to-end latency, frequency, CPU(Central Processing Unit, 중앙처리장치), queue backlog를 숫자로 측정한다.

52.1 성능 문제를 네 숫자로 바꾼다#

rate(Hz), latency(ms), jitter(ms), resource(CPU/memory)를 구분한다. 30 Hz가 유지되어도 각 frame이 800 ms 늦게 처리된다면 control에는 쓸 수 없다. 평균 latency만 보고 긴 tail latency를 놓치면 간헐적인 robot stop 원인을 찾기 어렵다.

52.2 CLI(Command-Line Interface, 명령줄 인터페이스)로 먼저 측정한다#

ros2 topic hz /camera/image_raw
ros2 topic delay /camera/image_raw
ros2 topic bw /camera/image_raw
top

CLI 측정은 상세 profiler가 아니라 어느 층을 더 볼 것인가를 결정하는 1차 진단이다. publisher rate가 이미 낮다면 upstream sensor/driver, rate는 정상인데 subscriber output만 늦다면 callback/queue/compute를 본다.

52.3 ros2 tracing을 언제 쓰는가#

callback이 언제 준비되고 실제로 언제 실행됐는지, publish/subscription 경로에서 지연이 어디에 생기는지 확인하려면 tracing이 필요하다. 시스템 로그는 의미 이벤트를, tracing은 실행 타이밍을 보여준다는 차이가 있다.

52.4 Queue backlog를 감지하는 패턴#

입력 message의 header stamp와 처리 시점 now 차이를 로그로 남기면 stale data가 누적되는지 볼 수 있다. 처리 시간이 주기보다 길면 depth를 키우기 전에 algorithm rate를 낮추거나 worker/process를 분리해야 한다.

52.5 최적화 순서#

  1. 정확한 병목을 측정한다.

  2. 불필요한 message copy/format conversion을 줄인다.

  3. callback blocking과 thread contention을 줄인다.

  4. 필요하면 node composition 또는 C++ 전환을 검토한다.

  5. 마지막에 CPU affinity/priority 같은 OS(Operating System, 운영체제) 튜닝을 검토한다.

53장. Multi-machine·Multi-robot·Clock·Namespace·Network#

전문가 목표 한 PC(Personal Computer, 개인용 컴퓨터) 데모에서 여러 PC·여러 로봇 시스템으로 확장할 때 discovery, 이름, clock, bandwidth 문제를 사전에 설계한다.

53.1 Multi-machine은 네트워크 문제가 먼저다#

두 PC가 같은 Topic을 보려면 먼저 IP route와 firewall, multicast/unicast discovery 환경이 맞아야 한다. 그 다음 ROS_DOMAIN_ID와 RMW 설정을 확인한다. 애플리케이션 topic 이름을 바꾸기 전에 네트워크 계층을 검증한다.

53.2 Multi-robot은 namespace만 붙이는 문제보다 크다#

`/robot1`, `/robot2` namespace로 이름 충돌을 피할 수 있지만 TF frame, map/odom 관계, parameter files, action server 이름, diagnostics까지 robot identity를 일관되게 관리해야 한다. 절대 topic 이름을 하드코딩한 node는 multi-robot 확장에서 바로 문제가 된다.

53.3 Clock synchronization#

여러 PC의 센서를 fusion하면 각 장비 timestamp의 기준이 중요하다. 100 ms clock offset은 움직이는 로봇에서 큰 공간오차로 바뀐다. system time을 쓸지 hardware timestamp를 쓸지, NTP/PTP를 어떤 수준으로 맞출지 요구 정확도에 맞게 정한다.

53.4 네트워크 bandwidth budget#

Raw Image, compressed Image, PointCloud2, LaserScan은 데이터율이 크게 다르다. 무선 네트워크에 모든 raw sensor를 그대로 broadcast하면 discovery가 성공해도 실제 처리 품질은 나빠질 수 있다. 필요한 곳에서만 고대역폭 topic을 구독하고 압축/전처리를 sensor 가까이 배치하는 구조가 유리하다.

53.5 WSL2(Windows Subsystem for Linux 2, Windows용 Linux 하위 시스템 2) 개발과 실제 robot LAN(Local Area Network, 근거리 통신망)을 분리해 검증#

WSL2에서는 Windows NIC와 Linux 네트워크 namespace 사이 경계가 추가된다. 로봇 연결 문제가 생기면 Windows에서 로봇 IP 도달 → WSL에서 도달 → DDS discovery → ROS Topic 데이터 순서로 확인하면 원인을 좁히기 쉽다.

54장. Plugin Architecture — Nav2·ros2_control을 확장하는 방법#

전문가 목표 기존 server를 포크하지 않고 algorithm plugin을 추가하는 ROS 2(Robot Operating System 2, 로봇 운영체제 2) 확장 패턴을 이해한다.

54.1 Framework와 Algorithm을 분리한다#

Nav2 planner/controller server는 lifecycle, action, costmap, parameter, plugin loading 같은 공통 인프라를 담당하고 실제 planning/control algorithm은 plugin으로 교체한다. 이 구조 덕분에 전체 navigation stack을 fork하지 않고 새 알고리즘을 추가할 수 있다.

54.2 Plugin이 가져야 할 수명주기#

실제 plugin은 configure/activate/deactivate/cleanup과 같은 lifecycle 문맥에서 resource를 준비·해제해야 한다. constructor에서 ROS(Robot Operating System, 로봇 운영체제) resource를 무조건 만들면 재구성이나 테스트가 어려워진다.

54.3 Planner plugin 사고방식#

Custom planner를 만든다면 입력은 start/goal과 costmap/model, 출력은 path라는 계약으로 생각한다. algorithm 내부는 A*, RRT, graph search 무엇이든 될 수 있지만 server와의 계약, parameter, exception/failure semantics는 framework 규칙을 따라야 한다.

54.4 ros2_control hardware/controller plugin#

ros2_control 역시 hardware interface와 controller plugin을 통해 상위 ROS graph와 실제 actuator 구현을 분리한다. 특정 vendor API가 바뀌더라도 hardware plugin 경계에서 흡수하면 상위 controller와 MoveIt2 연결을 유지할 수 있다.

54.5 전문가 과제#

  1. Nav2 planner/controller server가 제공하는 action과 plugin 역할을 별도 그림으로 그린다.

  2. 기존 plugin 하나의 parameter YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)과 source interface를 읽고 lifecycle callback을 표시한다.

  3. 새 plugin을 만든다고 가정하고 입력·출력·failure semantics·parameter를 먼저 문서화한다.

55장. 테스트·CI·버전·배포·품질 선언#

전문가 목표 ROS 2(Robot Operating System 2, 로봇 운영체제 2) package를 개인 데모가 아니라 재현·검증·업데이트 가능한 소프트웨어 자산으로 운영한다.

약어 표기 CI: Continuous Integration(지속적 통합)

55.1 테스트 피라미드#

순수 수학/변환 함수는 빠른 unit test, Node interface는 launch/integration test, 전체 robot behavior는 simulation/system test로 나눈다. 모든 것을 Gazebo E2E test로만 검증하면 느리고 원인분리가 어렵다.

55.2 launch_testing과 fixture#

Node가 실제로 뜨고 Topic/Service/Action 계약을 지키는지는 launch 기반 integration test가 유용하다. rosbag을 sensor fixture로 사용하면 현장 데이터를 반복 실험으로 바꿀 수 있다.

55.3 Interface versioning#

Custom msg/srv/action 변경은 여러 package에 영향을 준다. 필드를 삭제하거나 의미를 바꾸는 breaking change는 의존 package와 bag/recorded data까지 고려해 버전 정책을 세운다. typed interface가 강력한 만큼 계약 변경 비용도 명시적이다.

55.4 CI가 확인해야 할 최소 항목#

  • clean workspace에서 rosdep 설치와 colcon build가 되는가

  • unit/integration test가 통과하는가

  • lint와 package metadata가 유효한가

  • README의 launch/parameter 예제가 실제로 동작하는가

  • 지원 ROS(Robot Operating System, 로봇 운영체제) distribution과 OS(Operating System, 운영체제)가 명확한가

Python 패키지 검사 명령: colcon test --packages-select my_pkg로 테스트를 실행한 다음 colcon test-result --all --verbose로 실패한 케이스와 원인을 확인한다.

CI에서는 ament 계열 lint, 단위 테스트, 노드 간 통합 테스트, 시뮬레이션 테스트를 구분해 기록한다. 다른 PC(Personal Computer, 개인용 컴퓨터)의 빈 workspace에서도 rosdep으로 의존성을 설치한 뒤 빌드·테스트·Launch 실행이 재현되어야 한다.

55.5 Production 배포 체크#

실제 로봇에서는 실행 순서, restart policy, 로그 보존, parameter version, calibration asset, network configuration, robot firmware/driver version을 함께 고정해야 한다. source commit 하나만 기록해서는 현장 상태를 재현할 수 없다.

Part XI. 전문가 사례연구#

사례연구 1. 하나의 ROS 2 패키지로 Topic·Service·Action·Parameter·Launch를 모두 연결하기#

이 사례연구의 목적은 앞에서 배운 인터페이스를 따로따로 실행하는 것이 아니라, 하나의 작은 “작업 시스템” 안에서 서로 어떤 역할을 맡는지 끝까지 연결해 보는 것이다. 예제는 가상의 검사 스테이션을 모델링한다. 상태는 Topic으로 흘리고, 모드 변경은 Service로 처리하며, 시간이 걸리는 검사 작업은 Action으로 실행하고, 주기와 threshold는 Parameter로 둔다.

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

CS1.1 시스템 계약부터 작성한다#

기능 ROS 인터페이스 타입/의미
장비 상태 /station/status Topic RobotStatus: ready, mode, progress
모드 변경 /station/set_mode Service 요청 mode → 성공/실패
검사 실행 /station/run_inspection Action goal item_id → feedback progress → result pass/fail
상태 발행주기 Parameter status_rate_hz
판정기준 Parameter score_threshold

이 표가 먼저 있어야 코드가 늘어나도 책임이 섞이지 않는다. 특히 “검사 실행”을 Service로 만들면 검사 중 진행률이나 취소를 표현하기 어려워진다는 점을 다시 확인한다.

CS1.2 Interface package#

station_interfaces/
├── CMakeLists.txt
├── package.xml
├── msg/StationStatus.msg
├── srv/SetMode.srv
└── action/RunInspection.action

# StationStatus.msg
builtin_interfaces/Time stamp
string mode
bool ready
float32 progress

# SetMode.srv
string mode
---
bool success
string message

# RunInspection.action
string item_id
---
bool passed
float32 score
string message
---
float32 progress
string stage

메시지에서 timestamp를 넣은 이유는 상태가 언제 유효했는지 추적하기 위해서다. 실제 센서 기반 검사라면 Header를 사용해 frame_id까지 포함할 수 있다.

CS1.3 CMakeLists.txt의 핵심#

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(builtin_interfaces REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/StationStatus.msg"
  "srv/SetMode.srv"
  "action/RunInspection.action"
  DEPENDENCIES builtin_interfaces
)

ament_export_dependencies(rosidl_default_runtime)
ament_package()

Interface 파일을 추가했는데 build 후 Python import가 안 된다면 `rosidl_generate_interfaces`, package.xml의 generator/runtime dependency, 그리고 workspace source 여부를 순서대로 확인한다.

CS1.4 Node 내부에서 인터페이스를 조합한다#

class StationNode(Node):
    def __init__(self):
        super().__init__('station')
        self.declare_parameter('status_rate_hz', 2.0)
        self.declare_parameter('score_threshold', 0.7)

        self.status_pub = self.create_publisher(
            StationStatus, '/station/status', 10)
        self.mode_srv = self.create_service(
            SetMode, '/station/set_mode', self.on_set_mode)
        self.action_server = ActionServer(
            self, RunInspection, '/station/run_inspection',
            execute_callback=self.execute_inspection)

        hz = self.get_parameter('status_rate_hz').value
        self.timer = self.create_timer(1.0 / hz, self.publish_status)

여기서 중요한 것은 “Node 하나에 인터페이스가 여러 개 있어도 된다”는 점과 “각 인터페이스의 의미는 여전히 분리되어 있어야 한다”는 점이다. 상태 발행과 장시간 검사 실행은 서로 다른 callback 경로다.

CS1.5 Action callback의 상태흐름#

async def execute_inspection(self, goal_handle):
    feedback = RunInspection.Feedback()
    for i, stage in enumerate(['capture', 'analyze', 'judge']):
        if goal_handle.is_cancel_requested:
            goal_handle.canceled()
            return RunInspection.Result(passed=False,
                                        score=0.0,
                                        message='canceled')
        feedback.progress = (i + 1) / 3.0
        feedback.stage = stage
        goal_handle.publish_feedback(feedback)
        await asyncio.sleep(0.5)

    score = self.compute_score()
    threshold = self.get_parameter('score_threshold').value
    result = RunInspection.Result()
    result.score = score
    result.passed = score >= threshold
    result.message = 'done'
    goal_handle.succeed()
    return result

async/await 해설 async def는 coroutine을 정의한다. coroutine은 중간에 await로 제어권을 돌려줄 수 있어 장시간 Action 실행을 한 함수로 표현하면서도 다른 이벤트가 처리될 여지를 만든다. 다만 async 함수 안에서도 time.sleep()처럼 thread를 실제로 막는 함수를 쓰면 비동기의 장점이 사라지므로 ROS timer나 asyncio-compatible 대기를 구분해야 한다.

검토 포인트 실제 CPU(Central Processing Unit, 중앙처리장치)-bound inference를 `asyncio.sleep` 자리에 그대로 넣으면 executor/event loop를 막을 수 있다. 무거운 연산은 별도 worker 또는 적절한 executor 구조로 분리한다.

CS1.6 Launch와 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)로 재현성 확보#

# config/station.yaml
station:
  ros__parameters:
    status_rate_hz: 5.0
    score_threshold: 0.82

# launch/station.launch.py
Node(
    package='station_app',
    executable='station_node',
    name='station',
    parameters=['config/station.yaml'],
)

CS1.7 검증 절차#

  1. `ros2 node info /station`으로 네 인터페이스가 모두 노출되는지 확인

  2. `ros2 topic echo /station/status`로 상태 stream 확인

  3. Service로 mode 변경 후 status Topic이 반영되는지 확인

  4. Action goal을 보내 feedback이 진행되는지 확인

  5. 실행 중 cancel을 보내 canceled 상태 확인

  6. parameter를 바꿔 threshold 동작이 코드 재빌드 없이 바뀌는지 확인

사례연구 결론 ROS 2 인터페이스는 각각 외워야 할 문법이 아니라 시스템의 서로 다른 시간적 의미를 표현하는 도구다. 이 예제를 스스로 다시 만들 수 있으면 Topic·Service·Action·Parameter·Launch의 역할을 통합적으로 이해한 것이다.

사례연구 2. TF2·URDF를 이용해 센서 좌표를 로봇 좌표로 바꾸기#

이번 사례는 “카메라가 base_link 앞 0.30 m, 위 0.45 m에 달려 있다”는 단순한 로봇을 모델링한다. URDF로 장착 위치를 표현하고 robot_state_publisher가 TF를 발행하게 한 뒤, 카메라 frame의 PointStamped를 base_link로 변환한다.

약어 표기 TF2: ROS 2 Transform Library(ROS 2 좌표변환 라이브러리) · URDF: Unified Robot Description Format(통합 로봇 기술 형식)

CS2.1 URDF/Xacro(XML Macros, XML 매크로) 모델#

<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="sensor_robot">
  <link name="base_link"/>
  <link name="camera_link"/>
  <joint name="camera_joint" type="fixed">
    <parent link="base_link"/>
    <child link="camera_link"/>
    <origin xyz="0.30 0 0.45" rpy="0 0 0"/>
  </joint>
</robot>

fixed joint이므로 이 변환은 static이다. 실제 RealSense처럼 `camera_link`와 optical frame이 별도로 존재하면 optical frame의 축 규약까지 모델에 포함해야 한다.

CS2.2 TF(Transform, 좌표변환) tree를 먼저 검증한다#

ros2 run tf2_ros tf2_echo base_link camera_link
ros2 run tf2_tools view_frames

`tf2_echo`에서 translation이 `(0.30, 0, 0.45)`인지 확인한다. 코드로 좌표를 변환하기 전에 TF tree가 맞는지 CLI(Command-Line Interface, 명령줄 인터페이스)에서 먼저 확인하는 습관이 중요하다.

CS2.3 숫자로 직접 계산해 본다#

카메라가 회전 없이 base와 같은 축을 사용한다고 가정하고, camera frame에서 점이 `(0.20, 0.10, 0.80)`이라면 base frame에서는 translation을 더해 `(0.50, 0.10, 1.25)`가 된다. 회전이 있는 순간 단순 덧셈은 틀리고 `R p + t`가 필요하다.

base𝐩=baseRcameracamera𝐩+base𝐭camera {}^{\mathrm{base}}\mathbf p={}^\mathrm{base}R_{\mathrm{camera}}\,{}^{\mathrm{camera}}\mathbf p+{}^\mathrm{base}\mathbf t_{\mathrm{camera}}
where
p위치를 나타내는 3×1 벡터
R카메라에서 본체 기준으로의 3×3 회전행렬
t같은 두 좌표계 사이의 3×1 평행이동 벡터 [m]

CS2.4 PointStamped를 쓰는 이유#

point = PointStamped()
point.header.frame_id = 'camera_link'
point.header.stamp = image_msg.header.stamp
point.point.x = 0.20
point.point.y = 0.10
point.point.z = 0.80

PointStamped는 숫자뿐 아니라 frame과 timestamp를 보존한다. 이미지가 촬영된 순간의 transform을 요청할 수 있기 때문에 움직이는 eye-in-hand 시스템에서 특히 중요하다.

CS2.5 일부러 오류를 만든다#

  1. camera joint의 x를 0.30에서 0.50으로 바꿔 물체 위치가 20 cm 이동하는지 확인

  2. timestamp를 의도적으로 오래된 값으로 만들어 TF extrapolation 오류 확인

  3. frame_id를 존재하지 않는 이름으로 바꿔 lookup 실패 확인

  4. static_transform_publisher와 URDF가 같은 transform을 동시에 발행하도록 만든 뒤 중복 publisher 문제 관찰

사례연구 결론 TF 문제를 코드 안의 좌표식 문제로만 보지 않는다. 모델(URDF), publisher, frame name, timestamp, buffer가 모두 계약의 일부다.

사례연구 3. 차동구동 Odometry를 계산하고 보정하기#

CS3.1 실제 숫자로 forward kinematics#

바퀴 반지름 `r=0.05 m`, track width `L=0.30 m`, 좌우 각속도 `ω_R=6 rad/s`, `ω_L=4 rad/s`라고 하자.

v=0.052(6+4)=0.25m/s,ω=0.050.30(6−4)≃0.333rad/s v=\frac{0.05}{2}(6+4)=0.25\;\mathrm{m/s},\quad \omega=\frac{0.05}{0.30}(6-4)\simeq0.333\;\mathrm{rad/s}
where
v, ω전진 선속도와 회전각속도
0.05, 0.30바퀴 반지름과 바퀴 간 거리 [m]
6, 4오른쪽과 왼쪽 바퀴 회전각속도 [rad/s]

1초 동안 속도가 일정하고 초기 heading이 0이라고 아주 거칠게 가정하면 직선 적분이 아니라 곡선운동을 한다. 작은 dt로 적분하는 이유가 여기 있다.

CS3.2 encoder tick에서 wheel angle까지#

encoder가 회전당 `N` tick을 준다면 tick 차이 `Δn`에서 wheel angle은 `Δφ = 2π Δn/N`, 이동거리는 `Δs=rΔφ`다. 좌우 이동거리 `Δs_R`, `Δs_L`에서 중심 이동과 heading 변화는 다음처럼 계산할 수 있다.

Δs=ΔsR+ΔsL2,Δθ=ΔsR−ΔsLL \Delta s=\frac{\Delta s_R+\Delta s_L}{2},\qquad \Delta\theta=\frac{\Delta s_R-\Delta s_L}{L}
where
Δs, Δθ로봇 중심의 이동거리 [m]와 회전각 변화량 [rad]
Δs_R, Δs_L오른쪽과 왼쪽 바퀴의 이동거리 [m]
L좌우 바퀴 간 거리 [m]

CS3.3 직선 보정과 회전 보정#

실제 2.000 m 직선을 명령했는데 odometry가 1.900 m라면 wheel scale이 약 5.26% 작게 추정되고 있을 수 있다. 반대로 제자리 360° 회전에서 heading이 340°만 증가한다면 track width 또는 좌우 scale 차이를 의심한다. 단, floor slip이 크면 한 번의 측정으로 기구 파라미터를 확정하지 않는다.

CS3.4 Calibration 실험 설계#

실험 주로 민감한 파라미터 측정 방법
긴 직선 왕복 평균 wheel radius scale 테이프/레이저 거리 측정
제자리 다회전 track width/좌우 차이 외부 각도 기준 또는 fiducial
큰 원 주행 scale + track width 결합 시작/끝 pose 오차
다른 바닥 반복 slip 영향 분산 비교

CS3.5 EKF(Extended Kalman Filter, 확장 칼만 필터)에 넣기 전 해야 할 것#

필터는 틀린 odometry calibration을 마법처럼 없애지 못한다. systematic bias가 큰 상태에서 IMU와 억지로 융합하면 필터가 센서끼리 충돌하는 모습을 보일 뿐이다. 먼저 kinematic calibration과 timestamp를 가능한 수준까지 바로잡고, 그 다음 covariance와 fusion을 튜닝한다.

사례연구 4. rosbag 하나로 SLAM 문제를 반복 재현하기#

약어 표기 SLAM: Simultaneous Localization and Mapping(동시 위치추정 및 지도작성)

CS4.1 기록 항목을 설계한다#

ros2 bag record /scan /odom /tf /tf_static /imu/data /joint_states

지도 결과 `/map`만 기록하면 입력 문제를 재현하기 어렵다. SLAM이 사용한 raw sensor와 pose 관계를 함께 기록해야 동일 조건을 다시 분석할 수 있다.

CS4.2 재생 환경#

  1. 분석용 workspace에서 SLAM node만 실행

  2. 모든 관련 노드 `use_sim_time=true` 설정

  3. `ros2 bag play <bag> --clock` 실행

  4. RViz에서 scan, odom, TF(Transform, 좌표변환), map을 동시에 관찰

  5. 파라미터 하나만 변경하고 같은 bag 재생

CS4.3 실험표를 만든다#

Run 변경 정량/정성 결과 판단
A baseline loop closure 성공, 벽 중첩 3 cm 기준
B scan_match threshold 증가 loop closure 감소 과도한 보수성 가능
C odom scale +3% 복도 끝 drift 증가 입력 odom 민감도 확인

같은 bag을 사용하면 “이번에는 사람이 다르게 운전해서 결과가 달라졌다”는 변수를 제거할 수 있다. 파라미터 튜닝에서 매우 중요하다.

CS4.4 지도 품질을 수치화하는 간단한 방법#

  • 폐회로 후 출발점 주변 벽의 중첩 오차

  • 알려진 두 벽 사이 실제 거리와 map 거리 비교

  • 직선 벽의 두께/휘어짐

  • 동일 bag 반복 시 결과 재현성

  • CPU(Central Processing Unit, 중앙처리장치) load와 scan 처리 rate

사례연구 결론 SLAM 튜닝은 “맵을 보고 느낌으로 조절”하는 작업이 아니라 입력을 고정하고 한 변수씩 바꾸는 실험이어야 한다.

사례연구 5. Nav2가 “안 간다”를 15분 안에 분해하는 절차#

CS5.1 증상을 계층으로 바꾼다#

“목표를 찍었는데 로봇이 안 간다”는 진단 가능한 증상이 아니다. 아래 질문으로 분해한다.

  1. NavigateToPose Action goal이 accept 되었는가?

  2. 현재 pose가 map 안에서 정상인가?

  3. global costmap에 start/goal이 free인가?

  4. planner가 path를 만들었는가?

  5. controller가 cmd_vel을 내는가?

  6. base driver가 cmd_vel을 실제 wheel command로 바꾸는가?

CS5.2 RViz에서 반드시 켜 둘 Display#

  • TF(Transform, 좌표변환)

  • Map

  • Global Costmap

  • Local Costmap

  • Global Path

  • Local/Controller trajectory 또는 관련 debug topic

  • LaserScan

  • RobotModel

  • ParticleCloud(AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정) 사용 시)

CS5.3 Global path가 없을 때#

Planner Server 로그, global costmap, footprint, goal 위치를 본다. 시작점/목표가 lethal obstacle 안에 있거나 inflation 때문에 통로가 닫혀 있을 수 있다. 이때 controller tuning은 아무 의미가 없다.

CS5.4 Path는 있는데 움직이지 않을 때#

Controller Server가 velocity를 계산하는지 `/cmd_vel`을 본다. `/cmd_vel`이 0이면 local costmap, goal/progress checker, controller constraint를 본다. command는 있는데 실제 로봇이 안 움직이면 base driver/hardware/safety 계층으로 내려간다.

CS5.5 진동할 때#

로봇이 좌우로 작은 회전을 반복하는 경우 controller만 의심하지 않는다. noisy localization, odom heading, 지나치게 작은 goal tolerance, footprint/obstacle flicker가 command 부호를 계속 바꾸게 할 수 있다. 먼저 pose와 local costmap이 안정적인지 본다.

CS5.6 최소 Nav2 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)을 읽는 법#

controller_server:
  ros__parameters:
    controller_frequency: 20.0
    controller_plugins: ["FollowPath"]
    FollowPath:
      plugin: "nav2_regulated_pure_pursuit_controller::RegulatedPurePursuitController"

local_costmap:
  local_costmap:
    ros__parameters:
      global_frame: odom
      robot_base_frame: base_link
      rolling_window: true
      plugins: ["obstacle_layer", "inflation_layer"]

YAML을 줄별로 외우지 않고 “누가 읽는가(controller_server/local_costmap), 어떤 frame을 기준으로 하는가, 어떤 plugin alias를 등록하는가”를 읽는다.

사례연구 6. 비전 기반 Pick을 단계별로 검증하기#

CS6.1 최종 Pick을 한 번에 테스트하지 않는다#

  1. 카메라 intrinsics로 pixel→3D만 검증

  2. 고정 물체를 두고 camera→base TF(Transform, 좌표변환)만 검증

  3. MoveIt2에 숫자로 target pose를 직접 넣어 planning만 검증

  4. Planning Scene collision object 검증

  5. gripper open/close 별도 검증

  6. 마지막에 perception result를 연결

통합 전에 각 경계를 독립적으로 검증하면 최종 실패가 발생했을 때 어느 변환/알고리즘이 문제인지 바로 좁힐 수 있다.

CS6.2 좌표 오차를 재는 표준 실험#

작업대 위에 3개 이상의 알려진 3D reference point를 놓고 camera가 추정한 base-frame 좌표와 실제 좌표를 비교한다. 평균 오차뿐 아니라 방향별 bias를 본다.

Point 실제 base xyz 추정 xyz 오차
P1 (0.50, 0.00, 0.10) (0.512,-0.006,0.104) 13.9 mm
P2 (0.40, 0.20, 0.10) (0.414,0.195,0.106) 16.0 mm
P3 (0.60,-0.20,0.10) (0.609,-0.214,0.102) 16.8 mm

모든 점에서 x 방향으로 비슷한 bias가 있으면 Hand-Eye translation/TCP(Tool Center Point, 공구 중심점)를 의심할 수 있고, 작업공간 끝으로 갈수록 오차가 커지면 회전 extrinsic이나 depth scale/robot kinematics를 의심할 수 있다.

CS6.3 planning 실패와 perception 실패를 분리한다#

인식 결과를 RViz Marker/Pose로 먼저 시각화하고, 정확한 target pose가 들어왔다는 것을 확인한 뒤 MoveIt2 planning을 본다. IK 실패, collision, joint limit, singularity는 perception과 다른 문제다.

CS6.4 E0509 실제 연결 전 dry-run#

  • virtual/simulation에서 동일 planning group 확인

  • 현재 joint state가 MoveIt2에 들어오는지 확인

  • trajectory controller 연결 확인

  • 낮은 속도/안전한 workspace에서 단일 pose 이동 검증

  • gripper/TCP/fixture collision 추가 후 재검증

사례연구 7. 로봇 프로젝트를 “데모”가 아니라 검증 가능한 시스템으로 마무리하기#

CS7.1 성공 기준을 먼저 수치로 정의한다#

“자율주행이 된다”, “컵을 잘 집는다”는 완료조건이 아니다. 같은 조건에서 반복했을 때 측정 가능한 지표가 필요하다.

기능 예시 지표
Nav2 20 goal 중 성공률, 평균/95% 이동시간, recovery 횟수
SLAM 폐회로 벽 중첩 오차, 알려진 거리 scale error
Localization 정지 상태 pose jitter, 재초기화 수렴시간
Pick 20회 grasp 성공률, position error, planning failure 비율
System CPU/GPU load, end-to-end latency, node crash 횟수

CS7.2 Failure taxonomy#

실패를 “실패” 한 종류로 기록하지 않는다. 예를 들어 Pick은 detection_missing, depth_invalid, tf_timeout, ik_fail, collision_no_plan, execution_abort, grasp_slip 등으로 나눈다. 그러면 가장 큰 병목이 어디인지 데이터로 알 수 있다.

CS7.3 실험 기록 템플릿#

run_id: 2026-09-26-001
scenario: red_cup_to_left_bin
software_version: git_sha
parameters: config_hash
result: failure
failure_code: tf_timeout
metrics:
  perception_latency_ms: 42
  planning_time_ms: null
artifacts:
  bag: bags/run001
  log: logs/run001.txt

이 수준의 기록이 있으면 “어제는 됐는데 오늘은 안 된다”를 Git revision, parameter, bag으로 재현할 수 있다.

CS7.4 최종 검토 질문#

  • 모든 주요 데이터에 frame과 timestamp가 있는가?

  • 같은 TF(Transform, 좌표변환)를 두 노드가 중복 publish하지 않는가?

  • 장시간 작업이 Action으로 표현되어 cancel 가능한가?

  • parameter가 코드에 하드코딩되어 있지 않은가?

  • 실패코드가 원인 계층을 구분하는가?

  • bag과 로그만으로 주요 실패를 재현할 수 있는가?

  • 시뮬레이션 성공과 실제 안전검증을 혼동하지 않는가?

사례연구 전체 결론 로봇 소프트웨어의 실력은 API(Application Programming Interface, 응용 프로그램 인터페이스)를 많이 외우는 데서 나오지 않는다. 시스템을 작은 계약으로 분해하고, 상태와 좌표를 추적하고, 실패를 재현 가능하게 만드는 능력에서 나온다.

Part XII. 로보틱스를 위한 AI·LLM·RAG·Agent#

첨부 교재 목차에서 반복적으로 다루는 프롬프트 엔지니어링, LLM 애플리케이션 개발, RAG, Agent, Open Source LLM, AI 페어 프로그래밍 영역을 현재 ROS 2 교재의 맥락에 맞춰 보강한다. 일반 챗봇 개발 자체가 목적이 아니라, 로봇 개발자가 자연어 모델을 안전하고 검증 가능한 소프트웨어 구성요소로 사용할 수 있게 하는 것이 목표다.

학습 원칙 LLM(Large Language Model, 대규모 언어 모델)을 먼저 ‘말 잘하는 모델’로 사용하지 않는다. 구조화 출력 → 검증 → Tool Calling → RAG(Retrieval-Augmented Generation, 검색증강생성) 근거 → ROS 2(Robot Operating System 2, 로봇 운영체제 2) Action/Service 연결 순으로 통제 범위를 넓힌다.

56장. 로봇 개발자가 알아야 할 LLM 핵심 개념#

이 장을 끝내면 토큰·컨텍스트·임베딩·Transformer·양자화·멀티모달 모델이 로봇 시스템 설계에 어떤 영향을 주는지 설명한다.

약어 표기 LLM: Large Language Model(대규모 언어 모델)

56.1 LLM은 다음 토큰을 예측하는 확률 모델이다#

LLM은 데이터베이스에서 정답 문장을 꺼내는 시스템이 아니다. 입력 토큰 시퀀스를 바탕으로 다음 토큰의 확률분포를 계산하고 이를 반복해 출력을 만든다. 따라서 같은 의미의 요청이라도 문맥, 예시, 제약, 샘플링 설정에 따라 결과가 달라질 수 있다. 로봇 제어에서는 이 비결정성을 actuator 명령과 직접 연결하지 않고, 구조화 출력과 validator를 사이에 둔다.

56.2 Token과 Context Window#

모델은 문자열을 토큰 단위로 처리한다. 컨텍스트에는 사용자 질문뿐 아니라 시스템 지침, 대화 기록, 검색 문서, tool 결과가 모두 들어간다. RAG에서 문서를 많이 넣는다고 항상 좋아지는 것은 아니다. 관련성이 낮은 chunk가 늘면 핵심 근거가 묻히고 비용과 지연이 커진다.

56.3 Transformer와 Attention을 실무 관점에서 읽기#

Transformer의 핵심은 입력 토큰들이 서로 어떤 관계를 가지는지 attention으로 가중 결합한다는 점이다. 개발자는 행렬식을 외우기보다 긴 입력에서도 모든 문장이 같은 비중으로 사용되지 않고, 중요한 단서끼리 경쟁한다는 특성을 이해하면 된다. 그래서 prompt와 retrieved context의 순서·명확성·중복이 결과에 영향을 준다.

56.4 Parameter 수, MoE, 양자화#

Parameter 수는 모델 용량의 한 지표지만 성능을 단독으로 결정하지 않는다. MoE는 전체 parameter 중 일부 expert만 활성화해 계산량을 줄일 수 있다. 양자화는 weight 정밀도를 낮춰 메모리를 줄이는 기술이다. 로컬 로봇 PC에서는 모델 크기뿐 아니라 KV cache, context 길이, CPU/GPU offload와 실제 token/s를 함께 본다.

56.5 Embedding은 생성 모델과 역할이 다르다#

Embedding 모델은 문장이나 문서를 벡터로 바꾸어 의미 유사도를 계산한다. RAG에서는 생성 LLM보다 먼저 검색 품질을 좌우하는 핵심 부품이다. ‘배터리 저전압 경고’와 ‘undervoltage alarm’처럼 표현이 달라도 의미가 비슷한 문장을 가까운 벡터로 만들 수 있어야 한다.

56.6 Temperature와 추론 강도#

Temperature는 출력 다양성을 조절하는 대표 파라미터다. 로봇 명령 파싱이나 JSON 생성은 창의성보다 일관성이 중요하므로 낮은 변동성이 적합하다. 일부 모델은 별도의 reasoning effort를 제공하지만, 추론을 오래 한다고 센서값이나 물리 안전조건을 자동으로 검증하는 것은 아니다.

56.7 Multimodal LLM과 로보틱스#

이미지·음성·텍스트를 함께 처리하는 모델은 현장 사진 설명, 자연어 작업지시, 매뉴얼 도면 질의에 유용하다. 그러나 픽셀을 정밀한 robot pose로 바꾸는 일은 여전히 camera calibration, depth, TF2, Hand-Eye 같은 기하 계층이 담당한다. 멀티모달 LLM은 perception의 의미 해석 계층이지 정밀 계측기 자체가 아니다.

57장. 프롬프트 엔지니어링: 자연어를 안정적인 인터페이스로 만들기#

이 장을 끝내면 프롬프트를 역할·목표·제약·입력·예시·출력 형식으로 분해하고 로봇 명령 파서를 테스트 가능한 형태로 설계한다.

57.1 좋은 프롬프트는 문장 센스보다 계약 설계다#

실무 프롬프트는 감성적인 주문이 아니라 API 계약에 가깝다. 모델의 역할, 해야 할 일, 사용할 수 있는 정보, 금지사항, 출력 스키마, 실패 시 행동을 명확히 한다. 로봇에서는 ‘모르면 추측하지 말고 오류 상태를 반환하라’와 같은 실패 계약이 특히 중요하다.

구성 예 목적
Role 로봇 명령 파서 업무 범위 고정
Goal 자연어를 이동 명령으로 변환 성공조건 정의
Constraints 허용 action만 사용, 속도 상한 안전·범위 제한
Context 현재 map/robot state 판단 근거
Examples 입력→정답 JSON 경계와 형식 고정
Output JSON Schema 프로그램 검증 가능

57.2 Zero-shot, Few-shot, Retrieval Context#

Zero-shot은 규칙만 주고 수행시키는 방식이고 Few-shot은 몇 개의 예시로 모델이 기대하는 경계를 구체화한다. 장비별 표현이 반복된다면 few-shot이 유용하다. 매뉴얼처럼 외부 사실이 필요하면 예시를 계속 늘리기보다 RAG로 필요한 문서를 검색해 넣는 편이 관리하기 쉽다.

57.3 데이터와 지시를 분리한다#

검색된 문서나 사용자 입력 안의 문장을 시스템 지시와 같은 권한으로 해석하면 prompt injection이 가능하다. 외부 문서는 ‘참고 데이터’로 취급하고, 그 안에 있는 지시문을 실행하지 않는다는 정책을 명확히 한다.

57.4 출력 형식을 스키마로 고정한다#

{
  "action": "navigate",
  "target": {"x": 1.2, "y": -0.4, "yaw": 0.0},
  "speed_limit": 0.3,
  "requires_confirmation": true
}

프로그램은 이 구조를 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) Schema 또는 typed model로 검증한다. 값 범위와 enum, 필수 field를 validation layer에서 다시 검사하면 자유문장을 직접 파싱하는 것보다 훨씬 안정적이다.

57.5 프롬프트 테스트는 정상 예제보다 경계조건이 중요하다#

  • 정상 명령: ‘2번 위치로 이동해’.

  • 모호한 명령: ‘저기로 가’.

  • 범위 초과: ‘최대 속도로 10 m 전진’.

  • 안전구역 밖 좌표.

  • 문서 속 prompt injection 문장.

  • 지원하지 않는 기능에 대한 요청.

57.6 프롬프트 버전 관리#

운영 프롬프트는 코드처럼 버전 관리한다. prompt text, model ID, tool schema, evaluation 결과를 함께 기록해야 변경 후 성능 저하를 추적할 수 있다. ‘좋아 보이는 답변 하나’보다 고정 평가셋의 통과율이 기준이 되어야 한다.

58장. LLM API·Structured Output·Tool Calling#

이 장을 끝내면 클라우드 LLM(Large Language Model, 대규모 언어 모델)을 프로그램에서 호출하고, 구조화 결과와 도구 호출을 통해 deterministic ROS(Robot Operating System, 로봇 운영체제) 코드와 연결한다.

약어 표기 LLM API: Large Language Model Application Programming Interface(대규모 언어 모델 응용 프로그램 인터페이스)

58.1 API(Application Programming Interface, 응용 프로그램 인터페이스) 호출의 최소 구조#

API key는 코드에 하드코딩하지 않고 환경변수 또는 secret manager에 둔다. timeout, retry, rate limit, 비용 한도는 애플리케이션 운영 요구사항으로 취급한다.

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

response = client.responses.create(
    model=os.environ["OPENAI_MODEL"],
    input="작업 지시를 한 문장으로 요약해줘."
)
print(response.output_text)

LLM API 호출 해설 client.responses.create(...)는 네트워크를 통해 모델 API에 요청을 보내는 라이브러리 호출이다. model은 사용할 모델 식별자, input은 사용자 입력이다. response.output_text는 SDK가 응답 구조에서 텍스트 출력을 편하게 추출해 주는 속성이다. 실제 애플리케이션에서는 timeout, retry, 오류코드, 구조화 출력 검증을 함께 설계한다.

환경변수/API 객체 해설 os.environ은 현재 프로세스가 받은 환경변수 사전이다. API key를 코드 문자열에 직접 적지 않고 환경변수에서 읽으면 소스 저장소에 비밀값이 남는 위험을 줄일 수 있다. OpenAI(...) 객체는 이후 API 요청에 공통 설정과 인증정보를 사용하는 client다.

58.2 Structured Output이 로봇에서 중요한 이유#

자유문장을 if문으로 다시 해석하는 것은 취약하다. JSON Schema 기반 structured output을 사용하면 field 이름, 타입, enum, required 항목을 강제할 수 있다. 최신 OpenAI Responses API도 JSON Schema 형식과 strict mode를 지원한다. 다만 스키마가 맞는 것과 물리적으로 안전한 것은 다른 문제이므로 별도 validator가 필요하다.

58.3 Tool Calling: 모델은 요청하고 프로그램이 실행한다#

Tool calling에서 모델은 함수 이름과 인자를 제안하고 실제 실행은 애플리케이션 코드가 수행한다. `navigate_to_pose(x,y)` 같은 도구를 제공하더라도 ROS Action 호출 전에 allowlist, 값 범위, 권한, confirmation 정책을 적용한다.

58.4 Streaming·비동기·Timeout#

대화 UI에서는 streaming이 유용하지만 로봇 동작은 출력이 완성되기 전에 실행하지 않는다. 긴 요청은 비동기로 처리하고 일정 시간 안에 응답하지 못하면 기존 안전 상태를 유지하도록 설계한다.

58.5 비용과 지연을 측정한다#

LLM 호출은 외부 네트워크 지연이 포함되므로 control loop에 넣지 않고 mission/task layer에 둔다. 요청당 token, 평균·95% latency, failure rate, retry count를 로그로 남기면 cloud/local 역할 분담을 근거 있게 결정할 수 있다.

59장. Open Source·Local LLM과 Ollama#

이 장을 끝내면 로컬 모델을 실행하고 ROS(Robot Operating System, 로봇 운영체제) event loop와 분리된 별도 추론 서비스로 사용할 수 있다.

약어 표기 LLM: Large Language Model(대규모 언어 모델)

59.1 로컬 LLM을 쓰는 이유#

네트워크 단절 환경, 내부 문서 보안, 반복 호출 비용, 일정한 응답 지연이 중요한 경우 로컬 LLM이 유리할 수 있다. 반면 모델 다운로드·GPU(Graphics Processing Unit, 그래픽처리장치) 메모리·추론 속도·업데이트 관리는 사용자의 책임이 된다.

59.2 모델 크기와 Quantization#

로컬에서는 parameter 수뿐 아니라 실제 weight 크기, KV cache, context 길이, quantization, CPU/GPU offload를 함께 본다. 4-bit 양자화는 메모리를 줄일 수 있지만 명령 파싱 정확도나 tool 선택 성능에 어떤 영향을 주는지 실제 평가셋으로 확인해야 한다.

59.3 Ollama API(Application Programming Interface, 응용 프로그램 인터페이스)#

Ollama는 설치 후 기본적으로 `http://localhost:11434/api`에서 API를 제공하며 generate, chat, embed endpoint를 제공한다. 로컬 모델을 별도 프로세스로 두고 ROS node가 HTTP로 호출하면 model lifecycle과 ROS graph를 느슨하게 분리할 수 있다.

curl http://localhost:11434/api/generate -d '{
  "model": "YOUR_LOCAL_MODEL",
  "prompt": "작업 지시를 구조화된 명령으로 바꿔줘.",
  "stream": false
}'

59.4 ROS callback 안에서 추론을 기다리지 않는다#

추론이 수백 ms~수초 걸릴 수 있으므로 subscriber callback에서 동기 HTTP 호출을 하고 결과를 기다리면 executor starvation이 생길 수 있다. worker thread/process, async client, queue, Action server를 사용해 ROS event loop와 추론을 분리한다.

59.5 Cloud + Local Hybrid#

간단한 intent 분류·민감문서 검색은 local model, 복잡한 언어 추론은 cloud model로 나눌 수 있다. fallback 경로에서도 동일한 output schema와 validator를 통과시키면 모델이 바뀌어도 ROS 쪽 계약을 유지할 수 있다.

60장. RAG 기초: 로봇 매뉴얼을 검색 가능한 지식으로 만들기#

이 장을 끝내면 문서 로딩→chunk→embedding→vector store→retrieval→generation의 전체 pipeline을 설계한다.

약어 표기 RAG: Retrieval-Augmented Generation(검색증강생성)

60.1 왜 Fine-tuning보다 RAG부터 보는가#

장비 매뉴얼과 작업표준은 자주 바뀌고 출처 제시가 중요하다. 이런 지식은 모델 weight에 학습시키기보다 검색 시점에 관련 문서를 가져오는 RAG가 관리하기 쉽다. 문서 교체가 곧 지식 업데이트가 되고, 답변 근거를 함께 제시할 수 있다.

60.2 RAG의 여섯 단계#

단계 입력 출력 주요 설계
Load PDF/MD/HTML/DB Document 파서, 표/그림 처리
Split Document Chunk 크기, overlap, 구조
Embed Chunk Vector embedding model
Store Vector+metadata Index vector DB
Retrieve Question Top-k chunk dense/hybrid/filter
Generate Question+context Answer 근거·인용 정책

60.3 Chunking이 검색 품질을 좌우한다#

고정 글자수만으로 자르면 표제·절·표가 분리될 수 있다. 로봇 매뉴얼에서는 오류코드 표, 안전 절차, parameter 설명이 의미 경계다. heading, page, model, revision 정보를 metadata로 보존하면 검색과 출처 제시에 유리하다.

60.4 Dense Retrieval과 Vector Store#

질문과 chunk를 embedding vector로 바꾸고 cosine similarity 등으로 가까운 문서를 찾는다. semantic similarity는 표현이 다른 질문을 잘 찾지만 정확한 모델명·오류코드·parameter 이름은 keyword/BM25 검색이 더 강할 수 있다.

60.5 Metadata Filter#

E0509, M0609, Nav2 문서가 섞여 있다면 모델명, software version, document type, revision date를 metadata로 저장하고 검색 전에 filter한다. retrieval 후 모델에게 ‘알아서 구분해’라고 맡기는 것보다 안정적이다.

60.6 답변은 근거와 함께#

운영용 RAG는 ‘검색된 근거만 사용’, ‘근거가 부족하면 부족하다고 답함’, ‘문서명·절·페이지를 함께 반환’ 같은 정책을 둔다. 모델 기억과 retrieval 근거를 섞지 않는 것이 중요하다.

61장. Advanced RAG: 검색 품질을 단계적으로 높이기#

이 장을 끝내면 Naive RAG 실패를 검색·재정렬·질의변환·라우팅·검증 단계로 분해하고 개선 기법을 선택한다.

약어 표기 RAG: Retrieval-Augmented Generation(검색증강생성)

61.1 Naive RAG의 실패 유형#

  • 필요한 문서가 top-k에 들어오지 않는다.

  • 관련 문서를 찾았지만 chunk가 너무 커 핵심이 묻힌다.

  • 오류코드처럼 exact term을 semantic search가 놓친다.

  • 모호한 질문이 잘못된 제품 문서를 검색한다.

  • 근거는 맞지만 생성 답변이 근거 밖 내용을 덧붙인다.

61.2 Hybrid Search: Dense + Sparse#

Dense retrieval은 의미 유사성, BM25 계열 sparse retrieval은 정확한 단어 일치에 강하다. 두 결과를 rank fusion으로 결합하면 ‘Alarm 2034’ 같은 exact term과 ‘모터 과부하 경고’ 같은 의미 질의를 동시에 대응하기 쉽다.

61.3 Reranker#

1차 retriever는 후보를 넓게 가져오고 cross-encoder 또는 LLM(Large Language Model, 대규모 언어 모델) reranker가 query와 chunk를 함께 보고 재정렬한다. retrieval recall은 넓게 확보하고 최종 context precision은 reranking으로 높이는 패턴이다.

61.4 Query Rewrite·Multi-query·HyDE#

질문을 검색에 적합하게 다시 쓰거나 여러 변형을 생성해 recall을 높일 수 있다. HyDE는 가상의 답변/문서를 먼저 만들어 그 embedding으로 검색하는 계열이다. 질의변환이 원래 의도를 바꾸지 않도록 원문 query도 유지한다.

61.5 Parent-Child·Hierarchical Retrieval#

작은 chunk로 검색하면 정확하지만 주변 문맥이 부족하고 큰 chunk는 문맥은 좋지만 검색이 둔하다. 작은 child chunk로 찾은 뒤 상위 parent section을 context로 반환하면 두 장점을 조합할 수 있다.

61.6 Contextual Compression과 Sentence Window#

검색된 긴 문서에서 질문과 관련된 문장만 추려 context를 압축하거나 매칭 문장 주변의 window를 제공하면 token을 줄이고 근거 밀도를 높일 수 있다.

61.7 Router와 Modular RAG#

질문이 ‘ROS2 API’, ‘E0509 알람’, ‘회사 작업표준’ 중 어디에 해당하는지 router가 먼저 판단해 서로 다른 retriever를 선택할 수 있다. SQL, vector DB, 웹 API를 하나의 검색방식으로 억지로 통합하지 않고 데이터 특성에 맞게 조합한다.

61.8 Corrective·Self-check RAG#

검색 결과가 불충분한지 평가하고 질의를 다시 작성하거나 다른 source를 찾는 corrective loop를 만들 수 있다. 생성 후에도 주장마다 근거가 있는지 검사한다. 정확도는 높일 수 있지만 latency와 비용이 늘어나므로 필요한 질문에만 쓴다.

61.9 RAG Evaluation#

정답률 하나로는 부족하다. Retrieval recall/precision, context relevance, answer groundedness, citation accuracy, latency, cost를 분리한다. 로봇 매뉴얼 질문 50~100개 정도의 고정 평가셋을 만들고 chunking이나 embedding 변경 때 회귀 테스트한다.

62장. LangChain·LlamaIndex를 구성요소로 이해하기#

이 장을 끝내면 프레임워크 API(Application Programming Interface, 응용 프로그램 인터페이스)보다 RAG(Retrieval-Augmented Generation, 검색증강생성)·Agent의 공통 데이터 흐름을 이해하고 교체 가능한 구조로 설계한다.

62.1 프레임워크를 먼저 고르지 않는다#

먼저 data source, chunking, retriever, reranker, generation, evaluation의 계약을 정하고 나중에 framework를 선택한다. API 문법은 바뀌어도 retrieval pipeline의 핵심 역할은 비교적 안정적이다.

62.2 LlamaIndex 관점: Ingestion과 Query Engine#

LlamaIndex는 Document→Node transformation, ingestion pipeline, vector store, retriever/query engine 같은 RAG 중심 추상화가 강하다. ingestion pipeline은 splitter, metadata extraction, embedding 같은 transformation을 순서대로 적용하고 cache할 수 있다.

62.3 LangChain/LangGraph 관점: Retrieval과 Agent Workflow#

LangChain 문서는 document loader, splitter, embedding, vector store, retriever를 modular building block으로 다루며 2-step RAG, Agentic RAG, Hybrid RAG를 구분한다. 더 복잡한 상태전이는 graph workflow로 표현할 수 있다.

62.4 로봇 프로젝트의 선택 기준#

요구 적합한 접근
단순 문서 Q&A 직접 구현 또는 가벼운 RAG framework
여러 loader/vector DB 실험 LlamaIndex/LangChain
여러 tool과 상태전이 Agent graph/workflow framework
안전·재현성 최우선 typed schema·validator·test 우선

62.5 Framework lock-in 줄이기#

retriever를 `retrieve(query)->documents`, LLM을 `generate(messages)->structured_result` 같은 자체 adapter 뒤에 숨기면 framework를 바꾸기 쉽다. ROS node가 LangChain object를 직접 주고받지 않도록 application boundary를 둔다.

63장. Autonomous Agent와 Tool-using Robotics#

이 장을 끝내면 Agent의 planner/tool/memory/state 개념을 ROS 2(Robot Operating System 2, 로봇 운영체제 2) Action·Service와 연결하고 자율화 범위를 설계한다.

63.1 Agent는 상태와 도구의 결합이다#

Agent는 목표를 받고 현재 상태를 보고, 필요하면 tool을 호출하고, 결과를 다시 관찰하며 다음 행동을 고른다. 로봇에서는 이 loop가 기존 FSM/BT/mission planner와 겹친다. LLM이 모든 상태기계를 대체해야 하는 것은 아니다.

63.2 Tool을 ROS 2 인터페이스로 매핑한다#

Agent Tool ROS 2 매핑 비고
get_robot_state Topic snapshot/Service read-only
navigate_to_pose Nav2 Action feedback/cancel
inspect_object Vision Service/Action sensor 상태
pick_object Manipulation Action 실행 전 validation
search_manual RAG retriever 물리 동작 없음

63.3 Planner와 Executor를 분리한다#

LLM은 ‘무엇을 할지’를 제안할 수 있지만 ‘어떻게 안전하게 실행할지’는 deterministic executor가 담당한다. LLM이 navigate tool을 고르면 validator가 좌표와 zone을 검사하고 Nav2 Action client가 실제 goal을 보낸다.

63.4 Memory는 세 종류로 나눈다#

  • 대화 memory: 최근 사용자 요청.

  • 작업 state: 현재 mission 단계와 성공/실패.

  • 지식 memory: 매뉴얼·작업표준·데이터베이스.

세 가지를 모두 긴 chat history에 넣지 않는다. 작업 state는 구조화된 저장소, 지식은 RAG(Retrieval-Augmented Generation, 검색증강생성), 대화는 필요한 요약만 유지하는 편이 안정적이다.

63.5 Human-in-the-loop#

고위험 tool은 confirmation gate를 둔다. 예를 들어 navigation은 자동, 실제 grasp는 작업자 승인 후 실행하도록 권한을 나눌 수 있다. 승인 여부는 prompt 문장보다 executor 정책에서 강제한다.

63.6 Agent Evaluation#

답변 정확도뿐 아니라 tool 선택 정확도, invalid argument rate, 불필요 tool call 수, mission success rate, cancel/timeout 처리, safety rejection rate를 측정한다.

64장. AI 페어 프로그래밍을 ROS 2 개발에 제대로 사용하기#

이 장을 끝내면 AI를 코드 생성기보다 설계·리뷰·테스트·문서화 파트너로 사용하고 ROS 2 특유 오류를 검증한다.

약어 표기 AI: Artificial Intelligence(인공지능) · ROS 2: Robot Operating System 2(로봇 운영체제 2)

64.1 먼저 시스템 계약을 준다#

‘ROS2 코드 짜줘’보다 package명, node 책임, subscribe/publish interface, QoS(Quality of Service, 서비스 품질), parameter, target distro를 먼저 명시한다. 그래야 AI가 임의 Topic 이름이나 오래된 API(Application Programming Interface, 응용 프로그램 인터페이스)를 만들 가능성이 줄어든다.

64.2 코드 설명과 리뷰#

처음 보는 package는 `package.xml→CMake/setup→entry point→Node interface→callback` 순으로 설명하게 하면 onboarding이 빨라진다. 단, AI 설명이 실제 코드와 일치하는지는 CLI(Command-Line Interface, 명령줄 인터페이스)와 source로 검증한다.

64.3 ROS 2 코드 리뷰 체크리스트#

  • callback 안에서 blocking sleep/HTTP(Hypertext Transfer Protocol, 하이퍼텍스트 전송 프로토콜)/inference를 하지 않는가

  • Topic 타입과 QoS가 실제 driver와 호환되는가

  • Header/frame/timestamp가 보존되는가

  • absolute topic name을 불필요하게 하드코딩하지 않았는가

  • parameter가 declare되고 범위가 명확한가

  • launch/YAML(YAML Ain't Markup Language, 데이터 직렬화 형식) 설치 규칙이 setup/CMake에 포함되는가

  • 장시간 작업은 Service보다 Action이 적합하지 않은가

  • shutdown/cancel/error path가 있는가

64.4 Bug Fix 프롬프트는 재현정보가 핵심이다#

에러 메시지 한 줄 대신 ROS(Robot Operating System, 로봇 운영체제) distro, OS(Operating System, 운영체제), 실행 명령, node/topic info, 관련 TF(Transform, 좌표변환), 예상 결과와 실제 결과를 함께 제공한다. rosbag 또는 최소 재현 package가 있으면 AI 리뷰 품질이 크게 높아진다.

64.5 Test 생성에 활용#

geometry transform의 known-answer test, message validation, lifecycle transition, action cancel test 같은 반복적인 scaffold를 AI로 빠르게 만들 수 있다. 사람이 기대값과 failure condition을 검토해야 한다.

64.6 문서 생성에 활용#

README, launch 사용법, parameter 표, troubleshooting table을 코드에서 추출해 초안을 만들 수 있다. 지원 distro, 실제 default parameter, hardware requirement는 source와 공식 문서에서 다시 확인한다.

65장. 통합 프로젝트 — 자연어 명령을 검증된 ROS 2 Action으로 연결하기#

이 장을 끝내면 사용자 자연어를 구조화 명령으로 만들고 validator와 dispatcher를 거쳐 Nav2/MoveIt2 Action으로 안전하게 연결한다.

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

65.1 전체 구조#

User text
   ↓
LLM / Structured Output
   ↓
Schema Validator
   ↓
Policy Validator (zone, speed, permission)
   ↓
ROS 2 Dispatcher
   ├─ navigate → Nav2 Action
   ├─ pick     → Manipulation Action
   └─ query    → RAG / Service
   ↓
Feedback / Result
   ↓
Human-readable response

LLM(Large Language Model, 대규모 언어 모델)은 의도 해석 계층이다. robot state, safety, motion execution은 기존 ROS 2 컴포넌트가 맡는다. 이 구조는 LLM을 교체해도 executor를 유지할 수 있고 잘못된 값은 validator가 차단할 수 있다.

65.2 명령 스키마 설계#

class RobotCommand:
    action: Literal["navigate", "pick", "query", "stop"]
    target_name: str | None
    pose: Pose2D | None
    speed_limit: float | None
    requires_confirmation: bool

각 action마다 required field가 다르므로 조건부 validation을 둔다. `stop`처럼 긴급하고 단순한 명령은 LLM 없이 deterministic channel로 우회하는 것도 좋은 설계다.

65.3 Policy Validator#

  • map boundary 안의 좌표인가

  • 금지구역/안전구역 조건을 만족하는가

  • speed_limit이 시스템 상한 이하인가

  • 현재 robot state가 해당 action을 받을 수 있는가

  • confirmation이 필요한 action인가

65.4 ROS(Robot Operating System, 로봇 운영체제) Dispatcher#

dispatcher는 문자열을 다시 해석하지 않는다. 검증된 typed command만 받아 Nav2/MoveIt2 Action client 또는 read-only Service/RAG tool로 매핑한다. Action feedback을 mission state로 기록하고 cancel/timeout을 명시적으로 처리한다.

65.5 RAG(Retrieval-Augmented Generation, 검색증강생성)를 어디에 붙일 것인가#

‘E0509 알람 204 해결법 알려줘’는 RAG query로 보내고 물리 동작은 하지 않는다. ‘알람 원인을 확인하고 필요하면 홈으로 복귀해’처럼 지식 질의와 행동이 섞이면 먼저 근거를 얻고 정책이 허용한 구조화 action만 실행한다.

65.6 실패와 로그#

실패 예 처리
Parse failure 스키마 생성 실패 재질문/거절
Policy failure 금지 좌표 실행 금지+이유
ROS failure goal rejected Action result 기록
Timeout LLM/Navigation 지연 cancel/safe idle
Knowledge failure 근거 없음 추측하지 않고 부족 표시

65.7 평가 시나리오#

1. 정상 이동 명령 20개에서 schema 변환 정확도를 측정한다.

2. 모호한 명령 10개가 실행되지 않고 clarification으로 전환되는지 확인한다.

3. 금지구역 명령이 validator에서 모두 차단되는지 확인한다.

4. Nav2 goal reject/cancel/timeout이 사용자 응답까지 올바르게 전달되는지 확인한다.

5. 매뉴얼 질의의 groundedness와 citation 정확도를 측정한다.

부록 A. Ubuntu 24.04 + ROS 2 Jazzy 설치와 핵심 CLI#

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2) · CLI: Command-Line Interface(명령줄 인터페이스)

A.1 Ubuntu 24.04에서 ROS(Robot Operating System, 로봇 운영체제) 2 Jazzy 설치#

아래 절차는 ROS 2 Jazzy의 Ubuntu 24.04(Noble) deb 설치 흐름을 따른다. 이미 `ros2 --help`가 정상이라면 건너뛰어도 된다.

locale
sudo apt update && sudo apt install -y locales software-properties-common curl
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8

sudo add-apt-repository universe
sudo apt update

export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F'"' '{print $4}')
curl -L -o /tmp/ros2-apt-source.deb "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"
sudo dpkg -i /tmp/ros2-apt-source.deb

sudo apt update
sudo apt install -y ros-jazzy-desktop ros-dev-tools
source /opt/ros/jazzy/setup.bash

A.2 매 터미널에서 source하기#

echo 'source /opt/ros/jazzy/setup.bash' >> ~/.bashrc
source ~/.bashrc

여러 workspace를 쓴다면 `/opt/ros/jazzy/setup.bash`를 먼저, 그 위에 각 workspace의 `install/setup.bash`를 source한다. 순서가 underlay→overlay 관계를 결정한다.

목적 명령
환경 source /opt/ros/jazzy/setup.bash ; source install/setup.bash
패키지 ros2 pkg list ; ros2 pkg executables <pkg>
노드 ros2 node list ; ros2 node info /node
Topic ros2 topic list -t ; echo ; info -v ; hz ; bw
Interface ros2 interface show <type>
Service ros2 service list -t ; call
Action ros2 action list -t ; info ; send_goal --feedback
Parameter ros2 param list/get/set/dump
Lifecycle ros2 lifecycle nodes/get/set
TF tf2_echo ; view_frames
Bag ros2 bag record/info/play
진단 ros2 doctor --report ; rqt_graph

부록 B. 최소 패키지 템플릿#

B.1 ament_python#

my_pkg/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/my_pkg
└── my_pkg/
    ├── __init__.py
    └── node.py
# setup.py 핵심
entry_points={
    'console_scripts': [
        'my_node = my_pkg.node:main',
    ],
}

B.2 Interface package#

robot_interfaces/
├── CMakeLists.txt
├── package.xml
├── msg/RobotStatus.msg
├── srv/SetMode.srv
└── action/MoveObject.action

B.3 Parameter YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)#

my_node:
  ros__parameters:
    rate_hz: 10.0
    frame_id: base_link
    threshold: 0.7

B.4 Launch#

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(package='my_pkg', executable='my_node',
             parameters=['config/my.yaml'])
    ])

부록 C. 로봇공학 수식 치트시트#

주제 식 의미
차동구동 v=r/2(ωR+ωL), ω=r/L(ωR-ωL) wheel speed ↔︎ body twist
평면 pose ẋ=v cosθ, ẏ=v sinθ, θ̇=ω odometry 적분
SE(3) p_A=T_A_B p_B frame 변환
역변환 R⁻¹=Rᵀ, t⁻¹=-Rᵀt transform 방향 반전
Quaternion x²+y²+z²+w²=1 단위 회전 표현
Covariance P=E[(x-μ)(x-μ)ᵀ] 상태 불확실성
Bayes p(x|z) ∝ p(z|x)p(x) 관측으로 belief 갱신
EKF predict P⁻=F P Fᵀ+Q 모델 불확실성 반영
Pose graph min Σ eᵀΩe 여러 상대제약 전역 최적화

부록 D. 확인문제 해설#

19장 — ROS(Robot Operating System, 로봇 운영체제) Graph#

Node는 ROS Graph의 논리적 기능 단위이고 프로세스는 OS(Operating System, 운영체제) 실행 단위다. 한 프로세스에 여러 Node가 들어갈 수 있다. 센서 스트림과 장시간 작업은 수명주기가 달라 Topic/Action을 구분한다.

20장 — Workspace·빌드#

underlay는 기반 ROS 설치, overlay는 그 위에 source한 workspace다. 현재 shell의 search path에 따라 어떤 package가 우선되는지가 달라진다.

21장 — Topic·QoS(Quality of Service, 서비스 품질)#

BEST_EFFORT는 최신성 중심 센서 stream에서 재전송 지연을 줄일 수 있다. Header는 frame/time이 의미 있는 sensor/pose 데이터에 중요하다.

22장 — Service·Action·Lifecycle#

Service는 빠른 request-response, Action은 goal-feedback-result-cancel 상태흐름이다. Lifecycle은 실행과 활성화를 분리한다.

46장 — Executor·Callback Group#

callback 안에서 같은 exclusive group이 처리해야 할 response를 동기 대기하면 deadlock이 가능하다. async와 group 분리로 피한다.

26장 — TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)·좌표계#

T_base_object = T_base_camera · T_camera_object. TF(Transform, 좌표변환) 오류는 frame 방향, publisher, timestamp/buffer를 함께 점검한다. TF는 좌표 변환, TF2는 이를 프레임·시간에 연결하는 ROS 2(Robot Operating System 2, 로봇 운영체제 2)의 조회 체계다. SO(3)는 회전만, SE(3)는 회전과 평행이동을 표현하며, 움직이는 로봇의 센서 좌표는 측정 시각에 맞는 transform으로 바꿔야 한다.

27장 — URDF(Unified Robot Description Format, 통합 로봇 기술 형식)·RViz#

URDF는 기구 구조를 표현한다. launch는 XML(Extensible Markup Language, 확장 마크업 언어) 문자열을 robot_state_publisher의 robot_description parameter에 전달하고, 이 Node는 joint state에서 TF를 계산한다. Description Topic을 쓸 때 CLI(Command-Line Interface, 명령줄 인터페이스)/RViz 구독자는 transient local QoS를 맞춰야 늦게 연결해도 마지막 모델을 받을 수 있다. 이 예제에서는 joint1이 Z축으로 base를 회전시키고 joint2와 joint3가 Y축으로 같은 평면에서 팔을 굽힌다.

28장 — Gazebo·ros2_control#

Gazebo는 물리를 계산하고 ros2_control은 command/state와 hardware/controller 경계를 표준화한다.

29장 — Odometry#

odom은 encoder 기반 연속 추정이라 drift한다. map은 global correction을 제공한다.

30장 — Kalman Filter#

공분산은 확률변수의 평균 중심 변동을 측정한다. P는 추정 오차, Q는 과정 잡음, R은 관측 잡음의 공분산이다. Q가 커지면 예측 모델을 덜 믿고 R이 커지면 측정을 덜 믿는 방향으로 가중치가 변한다. 상관된 센서 정보를 독립 측정처럼 반복 융합하면 불확실성을 과소평가할 수 있다. 1 m/s에서 100 ms의 timestamp 오차는 약 10 cm의 위치 대응 오차가 된다.

33장 — SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) 개념#

Pose graph node는 pose, edge는 상대변환 constraint다. loop closure는 장기 drift를 전역적으로 고친다.

34장 — Cartographer와 TurtleBot3 지도 작성#

SLAM 이상은 scan/TF/odom/time부터 확인하고 알고리즘 파라미터는 그 다음이다.

OccupancyGrid는 행 우선 1차원 data를 사용하며 길이는 width × height다. 4 × 4 예제의 길이는 16이고, 인덱스는 y × width + x다. -1/0/100은 각각 미관측/빈 공간/점유 공간을 뜻한다.

35장 — AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정)#

AMCL은 odometry로 particle을 예측하고 map과 scan 일치도로 weight를 갱신한다. map→odom으로 전역 보정을 표현한다.

param dump는 실행값의 스냅샷을 파일로 저장한다. 파일 수정은 자동 적용이 아니므로 Launch에서 다시 읽히게 하거나 동적 파라미터 변경 절차를 사용해야 한다.

36장 — Nav2 구성#

planner는 global path, controller는 local command. Costmap/footprint가 둘의 공통 환경표현이다.

37장 — Behavior Tree#

BT(Behavior Tree, 행동 트리)는 plan/follow/recovery의 장시간 흐름을 조정하고 lifecycle manager는 server 준비순서를 관리한다.

38장 — Nav2 API(Application Programming Interface, 응용 프로그램 인터페이스)#

mission logic은 controller가 아니라 상위 task layer에서 Action 결과를 조합해야 재사용성과 실패처리가 좋아진다.

42장 — MoveIt2#

URDF는 물리 구조를, SRDF(Semantic Robot Description Format, 의미론적 로봇 기술 형식)는 planning group과 semantic 정보를 제공한다. Planning Scene에 작업대와 장애물을 넣어야 충돌을 고려한 경로를 계획할 수 있지만, MoveIt2의 collision checking은 비상정지·보호정지 같은 산업용 안전기능을 대체하지 않는다. MoveJ는 관절공간 경로, MoveL은 TCP(Tool Center Point, 공구 중심점) 직선 경로를 만들며, 상대 이동 방향은 base 또는 tool 기준에 따라 달라진다. DART의 서보 온은 동작 가능 상태를 만들 뿐 안전 승인이 아니므로 핸드가이딩과 조그는 저속에서 사용한다. ZYZ 오일러 각은 Rz(A)-Ry(B)-Rz(C) 순으로 합성하며, 특이점 부근의 MoveL은 큰 관절 속도나 제한 오류를 일으킬 수 있어 자세·waypoint·명령을 조정한다. Modbus의 40257은 holding register 표기이고 PDU(Protocol Data Unit, 프로토콜 데이터 단위) 주소 256은 0-based 주소일 수 있다. 블록 쌓기의 파지·놓기 구간에서는 waypoint를 건너뛰지 않도록 블렌딩을 줄인다.

43장 — Hand-Eye·3D 비전#

pixel+depth를 camera 3D로 만든 뒤 calibrated TF로 base frame으로 변환한다. 전체 오차는 여러 calibration/noise source의 합이다.

47장 — 로그·성능 진단#

bag은 입력을 재현 가능하게 만들며 latency와 throughput은 별도로 측정한다. WSL 문제는 네트워크/USB(Universal Serial Bus, 범용 직렬 버스)/ROS 계층을 분리한다.

48장 — 시스템 통합#

통합 설계는 Graph, Frame, State, Contract를 문서화하고 성공률·시간·실패 원인을 측정한다.

부록 E. 공식 참고자료#

ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy Documentation — https://docs.ros.org/en/jazzy/

ROS 2 Tutorials / Concepts — https://docs.ros.org/en/jazzy/Tutorials.html

Nav2 Jazzy Documentation — https://docs.nav2.org/jazzy/

Nav2 Navigation Concepts / First-Time Robot Setup / Tuning Guide — https://docs.nav2.org/jazzy/

SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) Toolbox — https://github.com/SteveMacenski/slam_toolbox (ROS 2 package documentation also available via docs.ros.org)

MoveIt 2 Documentation — https://moveit.picknik.ai/

robot_localization — ROS 2 package documentation / repository

Doosan Robotics ROS2 — https://github.com/DoosanRobotics/doosan-robot2

Microsoft WSL Documentation — https://learn.microsoft.com/windows/wsl/

버전 주의 ROS 2와 Nav2는 배포판마다 기본 파라미터·plugin·launch 구성이 달라질 수 있다. 이 교재는 Jazzy 기준 개념과 공식 문서 구조를 사용하며, 실제 명령은 설치된 package 버전에서 `--help`, parameter dump, 공식 Jazzy 문서를 함께 확인한다.

ROBOTIS TurtleBot3 SLAM — https://docs.robotis.com/docs/systems/turtlebot3/slam?ros=jazzy

ROBOTIS TurtleBot3 Navigation — https://docs.robotis.com/docs/systems/turtlebot3/navigation/

ROBOTIS TurtleBot3 Simulation — SLAM Simulation · Navigation Simulation

ROS 2 Jazzy nav_msgs/OccupancyGrid — https://docs.ros.org/en/jazzy/p/nav_msgs/msg/OccupancyGrid.html

Nav2 AMCL(Adaptive Monte Carlo Localization, 적응형 몬테카를로 위치추정) Configuration Guide — https://docs.nav2.org/configuration/packages/configuring-amcl.html

부록 F. 실습 코드 구조와 자료#

F.1 제공 실습코드 구조#

ros2_course_ws/
├── README.md
└── src/
    ├── r2lab_interfaces/
    │   ├── msg/TrackedTarget.msg
    │   ├── srv/SetMode.srv
    │   └── action/TimedMove.action
    └── r2lab_nodes/
        ├── r2lab_nodes/
        │   ├── heartbeat.py
        │   ├── number_pub.py / number_sub.py
        │   ├── mode_server.py / mode_client.py
        │   ├── timed_move_server.py / timed_move_client.py
        │   ├── bump_go.py
        │   ├── tf_obstacle_detector.py
        │   ├── vff_avoidance.py
        │   ├── lifecycle_target.py
        │   ├── nav_to_pose.py / patrol.py
        │   └── executor_demo.py
        ├── launch/basics.launch.py
        ├── config/bump_go.yaml
        ├── behavior_trees/simple_nav.xml
        └── test/test_vff_math.py

F.2 실습 모듈의 진행 순서#

Francisco Martín Rico, A Concise Introduction to Robot Programming with ROS 2, 2nd Edition (CRC Press/Routledge, 2025)의 공개 목차와 공식 코드 저장소가 보여주는 “작은 Node에서 시작해 FSM → TF → reactive behavior → Behavior Tree/Nav2 → deep ROS 2로 진행하는 프로젝트 기반 학습 순서”를 구조적 참고점으로 사용했다. 본 교재의 한국어 설명, 예제 코드, 프로젝트 구성은 현재 ROS 2 Jazzy/WSL2 환경에 맞춰 별도로 작성했다.

F.3 버전 민감도가 높은 공식 자료#

  • ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy Documentation — https://docs.ros.org/en/jazzy/

  • Nav2 Jazzy Quickstart — https://docs.nav2.org/jazzy/getting_started/quickstart/quickstart/

  • Nav2 First-Time Robot Setup Guide — https://docs.nav2.org/jazzy/configuration_and_development/first_time_robot_setup_guide/

  • Nav2 Navigating while Mapping — https://docs.nav2.org/jazzy/tutorials/general_tutorials/navigation2_with_slam/navigation2_with_slam/

  • SLAM(Simultaneous Localization and Mapping, 동시 위치추정 및 지도작성) Toolbox — https://github.com/SteveMacenski/slam_toolbox

  • 책 공식 코드 저장소 — https://github.com/fmrico/book_ros2 (Jazzy: jazzy-devel branch)

부록 G. 기술 참고자료#

  • ROS 2(Robot Operating System 2, 로봇 운영체제 2) Jazzy Ubuntu deb 설치 문서 — Ubuntu 24.04(Noble), ros-jazzy-desktop, ros-dev-tools

  • ROS 2 Jazzy composition 문서 — 동일 process node composition과 marshalling 비용

  • Nav2 Jazzy Quickstart — modern Gazebo 기반 simulation

  • Nav2 First-Time Robot Setup Guide — TF(Transform, 좌표변환) → URDF(Unified Robot Description Format, 통합 로봇 기술 형식)/SDF(Simulation Description Format, 시뮬레이션 기술 형식) → Odometry → Sensors → Mapping/Localization → Footprint → Planner/Controller

  • Nav2 Configuration & Development — plugin registry, tuning, Simple Commander API(Application Programming Interface, 응용 프로그램 인터페이스)

  • A Concise Introduction to Robot Programming with ROS 2, 2nd ed. 공개 목차 — First Steps → FSM(Finite State Machine, 유한 상태 기계) → TF → Reactive Behaviors → Behavior Trees → Navigation → Deep ROS 2 단계적 흐름

부록 H. AI·LLM·RAG 실무 치트시트#

약어 표기 AI: Artificial Intelligence(인공지능) · LLM: Large Language Model(대규모 언어 모델) · RAG: Retrieval-Augmented Generation(검색증강생성)

H.1 문제별 우선 도구#

문제 우선 도구 피해야 할 것
자연어→명령 Structured Output + validator 자유문장 파싱
매뉴얼 Q&A 2-step RAG 모델 기억만 의존
여러 데이터원 Router/Agentic RAG 한 vector DB에 강제 통합
복잡 mission BT/FSM + LLM planner LLM이 servo loop 수행
오프라인 환경 Local LLM/Ollama 클라우드만 의존
고속 제어 deterministic controller LLM을 제어 loop에 삽입

H.2 RAG 개선 순서#

1. 평가셋을 먼저 만든다.

2. 문서 파싱과 metadata를 검증한다.

3. chunking을 조정한다.

4. dense retrieval baseline을 측정한다.

5. hybrid search를 비교한다.

6. reranker를 추가한다.

7. query rewrite/router를 필요한 질의에만 추가한다.

8. groundedness와 citation을 평가한다.

H.3 기준 자료#

https://platform.openai.com/docs/quickstart/make-your-first-api-request

https://platform.openai.com/docs/api-reference/responses

https://docs.ollama.com/api/introduction

https://docs.langchain.com/oss/python/deepagents/retrieval

https://docs.llamaindex.ai/

부록 I. ROS 2 Jazzy 전문 레퍼런스#

이 Part는 ROS 2 Jazzy 공식 문서의 Tutorials, Concepts, How-To Guides를 현재 교재와 대조해, 실습 과정에서는 자주 생략되지만 실제 개발·운영에서 중요한 항목을 보강한다. 앞 장의 내용을 대체하지 않고 필요할 때 찾아보는 레퍼런스 성격으로 구성한다.

약어 표기 ROS 2: Robot Operating System 2(로봇 운영체제 2)

I.1 Node 실행 인자·Remapping·Parameter·Logging 운용#

이 장의 목표는 같은 executable을 코드 수정 없이 다른 이름·namespace·Topic·parameter·log 설정으로 배포하는 방법을 이해하는 것이다.

I.1.1 모든 ROS(Robot Operating System, 로봇 운영체제) 전용 CLI(Command-Line Interface, 명령줄 인터페이스) 인자는 --ros-args 뒤에 둔다#

ROS 2 노드는 일반 프로그램 인자와 ROS 전용 인자를 함께 받을 수 있다. ROS 전용 remapping, parameter, logger 설정은 `--ros-args` 뒤에 둔다. 이 경계를 알면 launch 없이도 실행 중 topology를 빠르게 바꿔 실험할 수 있다.

ros2 run demo_nodes_py talker --ros-args \

-r __node:=my_talker \

-r __ns:=/demo \

-r chatter:=my_topic

위 실행에서 node 이름은 `/demo/my_talker`, Topic은 `/demo/my_topic`이 된다. Node/namespace remap은 Topic remap보다 먼저 적용된다. ROS 2의 이름 remapping은 실행 시 결정되는 static remapping이며, 실행 후 이름을 동적으로 바꾸는 기능으로 생각하면 안 된다.

I.1.2 한 process에 여러 Node가 있을 때 scoped remapping#

Composition처럼 한 process 안에 여러 Node가 있으면 remap을 특정 Node에만 적용할 수 있다. 전체 process에 무차별 적용하면 다른 Node의 이름까지 바뀌어 Graph가 예상과 달라질 수 있다.

ros2 run composition manual_composition --ros-args \

-r talker:__node:=my_talker \

-r my_talker:chatter:=my_topic

I.1.3 Parameter를 CLI와 YAML(YAML Ain't Markup Language, 데이터 직렬화 형식)로 주입한다#

단일 값은 `-p name:=value`로, 여러 설정은 YAML 파일로 주입한다. 배포용 시스템에서는 명령행에 긴 parameter를 늘어놓기보다 YAML을 버전 관리하고 launch에서 읽는 편이 재현성이 좋다.

ros2 run my_pkg my_node --ros-args -p max_speed:=0.3 \

--params-file config/robot.yaml

I.1.4 Logging은 console만의 기능이 아니다#

ROS 2 기본 logging은 console(stderr), 디스크 log 파일, `/rosout` Topic으로 전달될 수 있다. Severity는 DEBUG, INFO, WARN, ERROR, FATAL 순이다. Logger 이름은 계층적이며 Node 이름·namespace와 연관된다. 현장 로그를 설계할 때는 상태전이와 goal id, frame, 오류코드처럼 재현에 필요한 정보를 남긴다.

ros2 run my_pkg my_node --ros-args --log-level debug

공식 기준: Node arguments — ROS 2 Jazzy

공식 기준: Logging and logger configuration — ROS 2 Jazzy

I.2 QoS 전체 모델과 rosbag2 QoS Override#

기존 장에서 Reliability와 Durability 중심으로 다뤘다면, 이 장에서는 Jazzy 공식 QoS(Quality of Service, 서비스 품질) 모델 전체를 레퍼런스 수준으로 정리한다.

I.2.1 QoS Profile은 여러 정책의 묶음이다#

대표 정책은 History, Depth, Reliability, Durability, Deadline, Lifespan, Liveliness, Lease Duration이다. History가 KEEP_LAST일 때 Depth가 queue 크기를 정하고, Deadline은 연속 샘플 사이의 기대 최대 간격, Lifespan은 메시지가 오래되어 폐기되기 전의 유효시간이다.

I.2.2 Liveliness와 Lease Duration#

Liveliness는 Publisher가 아직 살아 있음을 판단하는 정책이다. Automatic에서는 관련 publisher의 활동이 생존 신호 역할을 하고, Manual by topic에서는 publisher가 직접 liveliness를 assert한다. Lease Duration을 넘도록 생존이 확인되지 않으면 liveliness lost로 판단할 수 있다. 주기 데이터가 ‘안 오는 것’과 publisher process가 죽은 것을 구별해야 하는 감시 시스템에서 유용하다.

I.2.3 Requested/Offered 호환성을 실제 연결 조건으로 본다#

Subscriber의 요구(request)와 Publisher의 제공(offer)이 호환되지 않으면 Topic 이름과 타입이 같아도 메시지가 전달되지 않는다. ROS 2는 sensor data 등 일반적인 용도를 위한 predefined QoS profile도 제공한다. 처음부터 모든 정책을 직접 지정하기보다 표준 profile에서 시작해 필요한 항목만 조정하는 편이 안전하다.

I.2.4 QoS Event를 오류 감지에 활용한다#

Deadline missed, liveliness changed/lost, incompatible QoS 같은 사건은 단순 packet loss와 다른 의미다. 통신 실패를 ‘데이터가 안 온다’ 한 문장으로 묶지 말고 endpoint 연결, QoS compatibility, deadline/liveliness event를 분리하면 원인을 더 빠르게 찾을 수 있다.

I.2.5 rosbag2도 QoS의 일부다#

rosbag2는 record/playback 시 발견한 endpoint의 QoS에 적응하지만, 특정 환경에서는 명시적인 override가 필요하다. 공식 Jazzy 가이드의 `--qos-profile-overrides-path`를 사용하면 Topic별 reliability, durability, history, depth, deadline, lifespan, liveliness 등을 YAML로 지정할 수 있다.

/talker:

reliability: best_effort

durability: volatile

history: keep_last

depth: 10

ros2 bag record -a -o my_bag \

--qos-profile-overrides-path qos_override.yaml

bag은 데이터만 저장하는 파일이 아니라 원래 시스템의 전달조건까지 고려해야 재현 실험이 정확하다. 센서 driver와 bag replay의 QoS가 다르면 ‘기록은 됐는데 재생 시 subscriber가 못 받는’ 현상이 생길 수 있다.

공식 기준: Quality of Service settings — ROS 2 Jazzy

공식 기준: rosbag2: Overriding QoS Policies — ROS 2 Jazzy

I.3 관찰가능성: Logging·Topic Statistics·ros2 doctor#

로봇 시스템의 품질은 문제가 없는 상태보다 문제가 생겼을 때 원인을 관찰할 수 있는 정도로 평가하는 편이 실용적이다.

I.3.1 Topic Statistics가 측정하는 두 가지#

ROS 2 Topic Statistics는 subscription 관점에서 Received Message Period와 Received Message Age를 측정할 수 있다. 각 window에서 평균, 최대, 최소, 표준편차, sample count를 계산한다. 기능은 기본적으로 비활성화되어 있고 subscription 설정에서 켜야 한다.

I.3.2 Message Age는 Header timestamp가 필요하다#

Message Age는 발행시각과 수신시각의 차이를 보려는 값이므로 message header에 timestamp가 있어야 한다. timestamp가 없으면 해당 통계를 계산할 수 없다. ‘30 Hz가 나온다’와 ‘30 Hz 데이터가 500 ms 늦게 온다’는 전혀 다른 시스템 상태다.

I.3.3 /statistics를 모니터링 시스템으로 연결한다#

Topic Statistics를 켜면 기본적으로 `statistics_msgs/msg/MetricsMessage` 형식의 측정값을 `/statistics`로 발행할 수 있다. 운영 환경에서는 camera/scan/odom의 period와 age를 함께 기록해 순간적인 backlog나 네트워크 지연을 수치로 남길 수 있다.

I.3.4 Logging과 rosbag을 시간축으로 합친다#

상태전이와 오류코드는 logger에, 센서·TF(Transform, 좌표변환)·odom은 rosbag에 남긴다. 둘의 timestamp를 맞추면 ‘어떤 sensor frame이 늦어진 직후 controller가 timeout되었는가’ 같은 인과관계를 재구성할 수 있다.

ros2 doctor --report
ros2 topic hz /scan
ros2 topic delay /camera/image_raw
ros2 topic bw /camera/image_raw

공식 기준: Topic statistics — ROS 2 Jazzy

I.4 SROS2와 DDS Security: 로봇 네트워크를 신뢰경계로 설계하기#

ROS_DOMAIN_ID나 namespace는 충돌 회피 수단이지 보안 경계가 아니다. 네트워크에 신뢰할 수 없는 participant가 존재할 수 있다면 인증·암호화·권한통제가 별도로 필요하다.

I.4.1 ROS 2 Security가 제공하는 것#

ROS 2 Security는 underlying DDS Security 기능을 이용해 participant 인증, 전송 데이터 암호화와 무결성, 통신 권한 제어를 제공한다. 보안 설정은 middleware가 읽는 인증서·권한 파일과 ROS 환경변수로 활성화한다.

I.4.2 Enclave는 보안정책의 배포 단위다#

Security enclave는 하나 이상의 Node/process/device에 적용할 identity와 permissions 정책을 묶는 단위다. 단순히 Node 이름 하나와 1:1로 생각할 필요는 없지만, 최소 권한 원칙에 맞게 통신 허용범위를 설계한다.

I.4.3 Keystore와 Private Key#

`sros2` 도구는 keystore와 enclave용 key/certificate/policy를 생성한다. CA private key는 새로운 participant와 정책을 서명할 수 있는 핵심 자산이므로 운영 robot에 무조건 두는 것이 아니라 조직의 PKI 정책에 맞춰 오프라인 보관 여부를 결정한다.

I.4.4 Jazzy 공식 튜토리얼 최소 실습#

mkdir -p ~/sros2_demo && cd ~/sros2_demo
ros2 security create_keystore demo_keystore

SROS2 CLI 해설 ros2 security 명령은 DDS Security에 필요한 key, certificate, governance/permissions 자료를 생성·관리하는 sros2 도구다. keystore는 여러 enclave의 보안자료를 모아두는 저장소이고, enclave는 특정 Node/보안정책 단위의 identity와 권한을 묶는다.

ros2 security create_enclave demo_keystore /talker_listener/talker
ros2 security create_enclave demo_keystore /talker_listener/listener
export ROS_SECURITY_KEYSTORE=~/sros2_demo/demo_keystore
export ROS_SECURITY_ENABLE=true
export ROS_SECURITY_STRATEGY=Enforce
ros2 run demo_nodes_py talker --ros-args --enclave /talker_listener/talker
ros2 run demo_nodes_py listener --ros-args --enclave /talker_listener/listener

보안이 켜진 상태에서 통신이 안 될 때는 Topic 이름만 볼 것이 아니라 양쪽 enclave identity, permissions, keystore 위치, middleware security support를 함께 확인한다. 공식 튜토리얼은 secure communication의 vendor 간 호환을 보장하지 않는 점도 명시한다.

공식 기준: ROS 2 Security — ROS 2 Jazzy

공식 기준: Setting up security with sros2 — ROS 2 Jazzy

I.5 TF2 심화: 시간·Stamped Data·MessageFilter#

TF2(ROS 2 Transform Library, ROS 2 좌표변환 라이브러리)의 핵심 문제는 ‘변환행렬이 있는가’뿐 아니라 ‘센서 메시지의 timestamp에 해당 transform이 준비되어 있는가’이다.

I.5.1 최신 transform으로 억지로 바꾸지 않는다#

움직이는 robot에서 sensor message의 시각과 transform 시각이 다르면 공간오차가 생긴다. `lookup_transform(..., Time())`로 최신값을 무조건 쓰면 extrapolation 오류는 사라질 수 있지만 측정시각의 pose라는 의미를 잃는다.

I.5.2 Time Travel은 과거 시점의 frame 관계를 조회하는 문제다#

TF2 buffer는 시간에 따른 transform history를 보관한다. 센서융합·tracking에서는 ‘현재 camera가 어디 있나’보다 ‘이 image가 찍힌 시점에 camera가 어디 있었나’가 중요하다. lookup 시 target/source frame뿐 아니라 query time도 인터페이스의 일부로 취급한다.

I.5.3 MessageFilter는 transform이 준비될 때까지 stamped message를 보류한다#

Camera, LaserScan, PointStamped처럼 Header가 있는 sensor message가 도착했지만 원하는 target frame으로 아직 변환할 수 없다면 즉시 버리거나 임의의 최신 transform을 쓰는 대신 MessageFilter 패턴을 사용할 수 있다. 공식 TF2 튜토리얼의 MessageFilter는 해당 stamped message를 cache하고 target transform이 가능해졌을 때 callback으로 넘긴다.

이 패턴은 sensor rate가 높고 TF가 약간 늦게 도착하는 distributed system에서 특히 유용하다. 다만 queue가 무한정 쌓이지 않도록 size와 timeout, drop 정책을 함께 설계해야 한다.

I.5.4 TF2 디버깅 체크 순서#

① source/target frame 이름이 존재하는가 → ② 누가 그 edge를 publish하는가 → ③ static/dynamic 책임이 맞는가 → ④ sensor stamp와 TF buffer 시간범위가 겹치는가 → ⑤ use_sim_time과 /clock이 일관적인가 순으로 확인한다.

ros2 run tf2_ros tf2_echo base_link camera_link
ros2 run tf2_tools view_frames

공식 기준: Using stamped datatypes with tf2_ros::MessageFilter — ROS 2 Jazzy

공식 기준: Debugging tf2 problems — ROS 2 Jazzy

I.6 Domain ID·RMW·Discovery Server·Multi-machine 운영#

여러 PC(Personal Computer, 개인용 컴퓨터)와 여러 robot이 한 LAN(Local Area Network, 근거리 통신망)에 들어오면 discovery 자체가 시스템 설계 대상이 된다.

I.6.1 ROS_DOMAIN_ID의 의미와 권장 범위#

같은 Domain ID의 participant는 기본적으로 서로 discover할 수 있고, 다른 Domain ID는 서로 분리된다. Jazzy 공식 Domain ID 문서의 짧은 권장사항은 일반적인 선택으로 0~101 범위를 사용하도록 안내한다. 단, OS별 ephemeral UDP port 범위와 한 host의 participant 수에 따라 더 세밀한 제약이 생길 수 있다.

I.6.2 RMW_IMPLEMENTATION을 바꾸는 것은 middleware를 바꾸는 일이다#

rclpy/rclcpp 위 애플리케이션은 RMW 추상화 덕분에 middleware vendor 의존성을 줄일 수 있다. 그러나 discovery, shared-memory, 보안, zero-copy, 설정파일과 성능특성은 구현별로 달라질 수 있으므로 RMW 변경은 동일 workload의 측정과 재검증을 동반해야 한다.

echo $RMW_IMPLEMENTATION
ros2 doctor --report

I.6.3 대규모 네트워크에서는 Discovery 방식도 선택한다#

기본 DDS discovery는 소규모 LAN에서 편하지만 많은 participant, VPN, Wi-Fi, multicast 제약이 있는 환경에서는 부담이 커질 수 있다. Jazzy Advanced Tutorial은 Discovery Server와 improved dynamic discovery를 별도 주제로 다룬다. 네트워크 정책상 multicast를 신뢰할 수 없는 환경에서는 vendor가 제공하는 discovery configuration을 검토한다.

I.6.4 Multi-robot에서 Domain과 Namespace의 역할을 분리한다#

Domain ID는 discovery 범위를 분리하고 namespace는 한 graph 안의 이름을 조직한다. 같은 fleet가 서로 통신해야 한다면 robot마다 Domain을 무조건 나누기보다 `/robot1`, `/robot2` namespace와 TF frame prefix, action/service 이름, parameter file을 일관되게 설계하는 편이 자연스럽다.

공식 기준: The ROS_DOMAIN_ID — ROS 2 Jazzy

공식 기준: Working with multiple RMW(ROS Middleware Interface, ROS 미들웨어 인터페이스) implementations — ROS 2 Jazzy

공식 기준: Discovery Server — ROS 2 Jazzy

I.7 ROS 2 내부 인터페이스와 Zero-copy 성능 최적화#

평소에는 rclpy/rclcpp만으로 충분하지만, 성능 병목이나 middleware 문제를 분석할 때는 rcl과 rmw의 경계를 알아야 한다.

I.7.1 Client Library → rcl → rmw → Middleware#

rclpy/rclcpp는 사용자-facing API다. 공통 C 계층인 rcl은 여러 client library가 공유하는 ROS 개념과 graph 접근을 제공하고, rmw는 underlying middleware의 최소 기능을 추상화한다. 실제 discovery, publish/subscribe, service request-reply, serialization은 middleware 구현과 type support가 맡는다.

I.7.2 rosidl에서 언어별 Message 코드가 만들어지는 과정#

`.msg/.srv/.action` 같은 interface 정의는 rosidl pipeline을 통해 Python에서 import할 수 있는 language-specific 코드와 middleware type support로 생성된다. 그래서 interface를 변경한 뒤에는 단순 Python 파일 수정과 달리 다시 build해야 한다.

I.7.3 Middleware-specific 설정은 portability 비용이 있다#

RMW 추상화의 목적은 애플리케이션이 특정 DDS vendor API를 직접 의존하지 않도록 하는 것이다. vendor-specific 객체와 설정으로 내려가면 특정 성능기능을 사용할 수 있지만 다른 RMW로 교체할 수 있는 portability가 줄어든다. 최적화 전후에 이 비용을 명시한다.

I.7.4 Loaned Message와 Zero-copy#

Loaned message는 middleware가 message memory를 할당·관리해 불필요한 allocation과 copy를 줄이는 메커니즘이다. 큰 Image/PointCloud를 고주파로 처리할 때 latency와 throughput에 도움이 될 수 있다. 다만 지원 여부는 RMW implementation과 message 형태에 따라 다르며, zero-copy를 썼다고 end-to-end copy가 자동으로 모두 사라지는 것은 아니다.

Jazzy 공식 Zero Copy 가이드는 loaned message가 RMW 지원을 필요로 하며, Fast DDS 계열 지원과 다른 구현의 지원상태가 다를 수 있음을 명시한다. 따라서 ‘zero-copy 사용’은 코드 한 줄이 아니라 RMW·shared-memory 설정·message type·process 배치를 포함한 시스템 최적화 항목으로 본다.

공식 기준: Internal ROS 2 interfaces — ROS 2 Jazzy

공식 기준: Configure Zero Copy Loaned Messages — ROS 2 Jazzy

I.7.5 시스템 운영 점검표#

새 ROS 2 시스템을 운영 레벨에서 검토할 때는 다음 순서로 확인한다. Graph와 node arguments → Topic type/QoS 전체 정책 → callback group/executor → frame/time → logging/statistics → rosbag 재현성 → test/lint → RMW/discovery → security → 성능최적화. 이 순서를 따르면 algorithm parameter를 만지기 전에 infrastructure 실패를 먼저 제거할 수 있다.