ROS2Lab Chapter 05
Ch.05

메시지 직접 만들기

표준 메시지 + 커스텀 .msg

⏱ 25분 #Message

🎯 이 챕터의 목표

Ch.04에서 우리는 std_msgs/msg/String이라는 이미 만들어진 메시지를 썼어요. 이번엔 표준 메시지의 진짜 모습을 들여다보고, 내가 원하는 형식의 커스텀 메시지를 처음부터 만들어봐요.

예를 들어 온도 센서 노드가 "섭씨 24.3도, 측정 위치 거실" 같은 메시지를 발행하려면, 그런 형식의 메시지를 직접 정의해야 해요.

📦 표준 메시지 패키지 4종

ROS2는 처음부터 자주 쓸 만한 메시지들을 묶음(패키지)으로 제공해요. 외워둘 만한 4개:

패키지
주 용도
자주 쓰는 타입
std_msgs
가장 기본형
String, Bool, Int32, Float64, Header
geometry_msgs
위치 / 자세 / 속도
Point, Pose, Twist, Vector3, Quaternion
sensor_msgs
센서 데이터
Image, LaserScan, Imu, PointCloud2, NavSatFix
nav_msgs
네비게이션
OccupancyGrid, Path, Odometry, MapMetaData
약어
Imu = Inertial Measurement Unit · 관성측정장치. 가속도/각속도/자세를 측정하는 센서. 스마트폰의 자이로 + 가속도계와 같은 거.
약어
LiDAR = Light Detection And Ranging · 빛(레이저)을 쏴서 거리를 재는 센서. 자율주행차의 핵심 센서. ROS2에서는 LaserScan 메시지로 표현.

🔍 ros2 interface 명령으로 들여다보기

표준 메시지가 어떻게 생겼는지 직접 봐요. 이 명령들은 자주 쓸 거예요.

# 사용 가능한 모든 메시지/서비스/액션 목록
ros2 interface list

# 특정 패키지의 인터페이스만
ros2 interface package std_msgs

# 메시지 정의 보기
ros2 interface show std_msgs/msg/String

# Pub할 때 쓸 템플릿 (YAML 형식)
ros2 interface proto geometry_msgs/msg/Twist

실제 자주 보는 메시지 3개 뜯어보기:

🔹 std_msgs/Header — 모든 센서 메시지의 시작

ros2 interface show std_msgs/msg/Header
# Standard metadata for higher-level stamped data types.
builtin_interfaces/Time stamp
        int32 sec
        uint32 nanosec
string frame_id

Header는 거의 모든 센서 메시지의 첫 필드예요. stamp는 측정 시각(시간동기화 핵심), frame_id는 좌표계 이름(예: "base_link", "camera_link").

🔹 sensor_msgs/LaserScan — LiDAR의 표준

ros2 interface show sensor_msgs/msg/LaserScan
std_msgs/Header header
float32 angle_min        # 시작 각도 (rad)
float32 angle_max        # 끝 각도 (rad)
float32 angle_increment  # 각도 분해능
float32 time_increment   # 측정 간 시간차
float32 scan_time        # 1회전 시간
float32 range_min        # 측정 최소 거리 (m)
float32 range_max        # 측정 최대 거리 (m)
float32[] ranges         # 거리 배열 (핵심!)
float32[] intensities    # 반사 강도

LiDAR 한 번 회전 분량의 거리 데이터가 ranges 배열에 들어있어요. 360도 360개 점이면 ranges[0]~ranges[359].

🔹 nav_msgs/Odometry — 로봇의 자기 위치

ros2 interface show nav_msgs/msg/Odometry
std_msgs/Header header
string child_frame_id
geometry_msgs/PoseWithCovariance pose
geometry_msgs/TwistWithCovariance twist

이동 로봇이 "내가 출발지에서 얼마나 왔는지" 발행하는 메시지예요. 자율주행의 핵심.

약어
Odometry = 라틴어 odo(길) + metry(측정). "주행거리계". 차량의 적산거리계처럼, 로봇이 출발 후 어디까지 왔는지 추정한 값.

📐 메시지 문법 — .msg 파일 읽는 법

.msg 파일은 매우 단순해요. 한 줄에 "타입 이름"씩 적으면 끝.

# Temperature.msg 예시
string location          # 측정 위치
float32 celsius          # 섭씨 온도
float32 humidity         # 습도 (%)
bool is_valid            # 측정 유효 여부
int32[] history          # 최근 10번의 측정값

기본 타입 (Primitive Types)

타입
의미
bool
참/거짓
is_valid
int8/16/32/64
부호 있는 정수
count
uint8/16/32/64
부호 없는 정수
nanosec
float32/64
실수
celsius, distance
string
문자열
location, frame_id
타입[]
가변 배열
float32[] ranges
타입[N]
고정 배열
float64[36] covariance
💡 다른 메시지를 필드로 포함시킬 수도 있어요: std_msgs/Header header처럼.

🛠 커스텀 메시지 만들기 — 7 STEP

이제 진짜로 만들어봐요. 온도 센서 메시지를 정의하고, 그걸 발행/구독하는 노드까지.

⚠️ 중요: Python 패키지(ament_python)에서는 메시지를 직접 정의할 수 없어요. 메시지 정의는 C++ 빌드 시스템(ament_cmake) 패키지에서 해야 해요. 처음엔 좀 헷갈리지만 ROS2의 규칙이에요.

STEP 1 — 메시지 전용 패키지 만들기

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 my_interfaces

이름 관례: <프로젝트>_interfaces 또는 <프로젝트>_msgs. 메시지 전용임을 표시.

STEP 2 — msg/ 폴더에 .msg 파일 만들기

cd ~/ros2_ws/src/my_interfaces
mkdir msg
nano msg/Temperature.msg

파일명 규칙: 첫 글자 대문자 + CamelCase. Temperature.msg O, temperature.msg X.

# Temperature.msg
std_msgs/Header header
string location
float32 celsius
float32 humidity
bool is_valid

STEP 3 — CMakeLists.txt 수정

~/ros2_ws/src/my_interfaces/CMakeLists.txt 파일을 열어 다음 부분을 추가/수정.

cmake_minimum_required(VERSION 3.8)
project(my_interfaces)

find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Temperature.msg"
  DEPENDENCIES std_msgs
)

ament_package()

핵심: rosidl_generate_interfaces 블록. 여기 .msg 파일들을 한 줄씩 나열해요. std_msgs/Header를 쓰니까 DEPENDENCIES std_msgs도 필수.

약어
rosidl = ROS Interface Definition Language · .msg/.srv/.action 파일을 받아 Python/C++ 코드로 자동 변환하는 도구. 우리가 직접 손댈 일은 없음.

STEP 4 — package.xml 수정

~/ros2_ws/src/my_interfaces/package.xml에 의존성과 멤버그룹 추가.

<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>

<depend>std_msgs</depend>

<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

<export>
  <build_type>ament_cmake</build_type>
</export>

STEP 5 — 빌드

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

STEP 6 — 확인

ros2 interface show my_interfaces/msg/Temperature
std_msgs/Header header
        builtin_interfaces/Time stamp
                int32 sec
                uint32 nanosec
        string frame_id
string location
float32 celsius
float32 humidity
bool is_valid

✅ 내가 만든 메시지가 표준처럼 작동해요. 이제 Pub/Sub에서 쓸 수 있어요.

STEP 7 — 커맨드라인으로 한 번 발행해보기

# 터미널 1
ros2 topic echo /room_temp

# 터미널 2 — 발행
ros2 topic pub --once /room_temp my_interfaces/msg/Temperature \
  "{location: '거실', celsius: 24.3, humidity: 55.0, is_valid: true}"
header:
  stamp: ...
  frame_id: ''
location: 거실
celsius: 24.3
humidity: 55.0
is_valid: true

🐍 만든 메시지로 Python 노드 작성

Ch.04에서 만든 my_first_pkg로 돌아가서 새 노드를 추가해봐요.

1) Publisher 노드 (temperature_pub.py)

cd ~/ros2_ws/src/my_first_pkg/my_first_pkg
nano temperature_pub.py
import random
import rclpy
from rclpy.node import Node
from my_interfaces.msg import Temperature


class TempPub(Node):
    def __init__(self):
        super().__init__('temperature_pub')
        self.pub = self.create_publisher(Temperature, '/room_temp', 10)
        self.timer = self.create_timer(2.0, self.tick)
        self.get_logger().info('온도 발행 시작!')

    def tick(self):
        msg = Temperature()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.header.frame_id = 'living_room'
        msg.location = '거실'
        msg.celsius = round(20.0 + random.random() * 8.0, 1)
        msg.humidity = round(40.0 + random.random() * 30.0, 1)
        msg.is_valid = True
        self.pub.publish(msg)
        self.get_logger().info(
            f'발행: {msg.celsius}°C / {msg.humidity}%'
        )


def main(args=None):
    rclpy.init(args=args)
    rclpy.spin(TempPub())
    rclpy.shutdown()


if __name__ == '__main__':
    main()

2) my_first_pkg/package.xml에 의존성 추가

이게 빠지면 빌드 시 "my_interfaces를 찾을 수 없다"는 에러가 나요.

<depend>my_interfaces</depend>

3) setup.py의 entry_points에 추가

'temperature_pub = my_first_pkg.temperature_pub:main',

4) 빌드 & 실행

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

# 실행
ros2 run my_first_pkg temperature_pub

다른 터미널에서 확인:

ros2 topic echo /room_temp
header:
  stamp: {sec: 1747645812, nanosec: 123456789}
  frame_id: living_room
location: 거실
celsius: 23.7
humidity: 58.2
is_valid: true
---

🎉 내가 정의한 메시지가 내 노드에서 실제로 흐르고 있어요.

📝 메시지 명명 관례

  • 패키지 이름: snake_case (예: my_interfaces)
  • 메시지 파일 이름: CamelCase.msg (예: RobotStatus.msg)
  • 필드 이름: snake_case (예: battery_level)
  • 패키지 분리: 메시지 전용 패키지는 보통 <프로젝트>_interfaces 또는 <프로젝트>_msgs
  • 의존 최소화: 메시지 패키지는 노드 패키지에 의존하지 않아야 함 (재사용 위해)

💡 언제 커스텀 메시지를 만들어야 하나?

  • 이미 표준 메시지에 있는 데이터 → 그냥 표준 쓰기. 속도면 Twist, 위치면 Pose, 거리면 LaserScan.
  • 여러 필드를 묶어서 한 메시지로 보내야 할 때. 예: 온도 + 습도 + 위치 + 유효성을 하나로.
  • 도메인 특화 데이터. 농업 로봇의 CropHealth, 의료 로봇의 PatientVital 등.
  • ⚠️ 커스텀 메시지는 그 패키지를 받는 쪽도 같이 빌드해야 함. 외부 호환성 떨어짐 → 표준이 있으면 표준 우선.
✏️ 퀴즈 1

로봇의 자세(위치+방향)를 보내려면 어떤 표준 메시지가 가장 적합한가요?

✏️ 퀴즈 2

커스텀 메시지를 정의하는 패키지는 어떤 빌드 타입이어야 하나요?

✏️ 퀴즈 3

.msg 파일 이름과 필드 이름의 표기 관례를 옳게 짝지은 것은?

✏️ 퀴즈 4

대부분의 센서 메시지에 가장 먼저 들어가는 필드는?

🎁 정리

  • 표준 메시지 4종: std_msgs / geometry_msgs / sensor_msgs / nav_msgs
  • ros2 interface show 타입으로 어떤 메시지든 정의를 들여다볼 수 있음
  • .msg 파일 한 줄 = "타입 이름"
  • 커스텀 메시지는 ament_cmake 패키지에서만 정의 가능 (Python 패키지에선 X)
  • 파일명은 CamelCase.msg, 필드명은 snake_case
  • 메시지 패키지 이름 관례: <프로젝트>_interfaces 또는 _msgs
  • 표준에 있으면 무조건 표준 우선, 정말 필요할 때만 커스텀

다음은 한 번에 노드 여러 개를 띄우는 Launch 파일이에요. 매번 터미널 5개 띄우긴 귀찮잖아요. 🚀