학습 목표

  • 표준 Interface를 먼저 조사하고 Custom Interface가 필요한 경우를 판단할 수 있다.
  • .msg, .srv, .action의 문법과 생성되는 Type을 설명할 수 있다.
  • Interface 전용 ament_cmake Package를 만들고 의존 Package에서 사용할 수 있다.
  • Python과 C++에서 생성 Type을 Publish·Subscribe하고 CLI로 검증할 수 있다.
  • Field, 단위, Timestamp, Frame, 배열 상한과 Error 표현을 실제 Robot 관점에서 설계할 수 있다.
  • Interface 변경의 호환성 위험을 분석하고 Version·Build·배포·CI 정책을 세울 수 있다.

1. Interface는 로봇 Software의 계약이다

ROS 2 Interface는 Process 사이에 전달할 Data의 이름, Type과 구조를 정의하는 계약입니다.

  • Message(msg): Topic에서 계속 흐르는 Data 구조
  • Service(srv): 짧은 Request와 Response 구조
  • Action(action): Goal, Result와 반복 Feedback 구조

Interface 파일은 실행 Code가 아니지만 Build 과정에서 Python·C++ Type, Type Support와 Middleware가 사용할 Metadata를 생성합니다. 따라서 Field 하나를 바꾸는 일은 단순한 Text 수정이 아니라 송신자·수신자·Bag·Bridge·도구가 공유하는 계약 변경입니다.

01요구사항과 단위 정의
02msg·srv·action 원본 작성
03rosidl Adapter와 Generator 실행
04언어별 Type과 Type Support 생성
05Publisher·Subscriber·Client·Server에서 사용
06DDS/RMW Serialization과 Wire Data 전달

2. 만들기 전에 표준 Interface부터 찾는다

Custom Type을 만들면 의미를 정확히 표현할 수 있지만 기존 도구와 Package가 자동으로 이해하지 못합니다. 먼저 설치된 표준 Type을 조사합니다.

ros2 interface packages
ros2 interface package sensor_msgs
ros2 interface list | grep -E 'geometry_msgs|sensor_msgs|nav_msgs'
ros2 interface show geometry_msgs/msg/Twist
ros2 interface show sensor_msgs/msg/BatteryState
필요한 Data 우선 검토할 Type 생태계 이점
이동 속도 geometry_msgs/msg/Twist Nav2·Teleop·Controller 연결
Laser Scan sensor_msgs/msg/LaserScan RViz2와 Sensor Tool 사용
Camera Image sensor_msgs/msg/Image cv_bridge·image_transport 사용
Odometry nav_msgs/msg/Odometry Localization·Navigation 연계
Battery sensor_msgs/msg/BatteryState 표준 Monitor와 Field 의미 공유
진단 diagnostic_msgs/msg/DiagnosticArray 진단 Aggregator·UI 활용
회사 전용 Gripper 명령 적절한 표준이 없을 수 있음 의미 있는 Custom Type 검토

다음 질문에 모두 답한 후 만듭니다.

  1. 설치된 표준 Package에 같은 의미가 있는가?
  2. 표준 Type의 조합이나 Wrapper로 충분한가?
  3. 기존 Tool이 반드시 읽어야 하는 Data인가?
  4. 단위·Frame·상태·제약이 표준 Type과 실제로 같은가?
  5. 장기간 소유하고 Version을 관리할 팀이 있는가?

std_msgs/msg/Float32처럼 의미 없는 Scalar에 Motor Mode나 Gripper 힘을 억지로 담는 것도 피해야 합니다. 0.7만으로는 비율·각도·속도·힘인지 알 수 없습니다. 표준 우선의미 있는 계약을 함께 지켜야 합니다.


3. 좋은 Interface를 만드는 설계 원칙

이름에 의미를 담는다

value, data1, flag 대신 max_effort, contact_detected, remaining_distance처럼 역할을 드러냅니다.

단위를 고정한다

가능하면 SI 단위를 사용하고 Comment에 [m], [rad/s], [N], [s]를 적습니다. Field 이름에 단위를 넣는 방식(distance_mm)은 Legacy Hardware 경계에서는 유용할 수 있지만, System 전체에서는 변환 누락을 유발할 수 있으므로 팀 규칙을 명시합니다.

시간과 Frame을 명시한다

공간·Sensor Data에는 std_msgs/Header를 검토합니다. stamp는 일반적으로 측정 시각, frame_id는 값이 표현된 Coordinate Frame이어야 합니다. Publish 시각과 측정 시각이 다르면 어떤 시각인지 주석에 적습니다.

상한을 설계한다

무제한 String·Sequence는 편하지만 최악의 Memory와 Serialized Size를 예측하기 어렵습니다. 실제 최대 Sensor 수·Waypoint 수를 알면 Bounded String과 Bounded Sequence를 검토합니다.

상태와 오류를 구분한다

bool success 하나로는 부분 성공, 거절, Timeout과 Hardware Fault를 구분하기 어렵습니다. 안정적인 상수 또는 Error Code, 사람이 읽는 Message와 Machine-readable 상태를 함께 설계합니다.

Command와 State를 분리한다

명령에 현재 상태를 섞거나 State에 요청값을 섞지 않습니다. Command가 끊겼을 때의 Timeout, Sequence ID와 Timestamp를 설계합니다.


4. msg 문법: Primitive, Field와 Comment

기본 형식은 type field_name입니다.

# msg/GripperCommand.msg
std_msgs/Header header
uint8 mode
float32 position
float32 max_effort
bool wait_for_contact
string operator_note

주요 Primitive Type:

범주 Type
논리 bool
정수 int8·uint8·int16·uint16·int32·uint32·int64·uint64
실수 float32·float64
문자·문자열 char·byte·string·wstring
다른 Interface geometry_msgs/Point, builtin_interfaces/Time

File 이름은 일반적으로 Upper Camel Case(GripperCommand.msg), Field는 Lower Snake Case(max_effort)를 사용합니다. 생성기 오류가 나면 File·Field·Constant Naming 규칙과 예약어를 먼저 확인합니다.

Comment는 생성된 Definition과 문서에 남는 계약의 일부입니다. 다음을 적습니다.

  • 단위와 허용 범위
  • 값 0이나 빈 배열의 의미
  • Timestamp가 나타내는 사건
  • Frame 의미
  • NaN·Unknown 표현 정책
  • 상수와 상태 전이 규칙

5. 배열, Sequence와 Bounded String

float32[] samples              # 길이 제한 없는 Sequence
float32[3] tip_offset          # 정확히 3개인 Array
int32[<=8] sensor_ids          # 최대 8개인 Bounded Sequence
string<=64 source_name         # 최대 길이가 제한된 String
표현 길이 설계 특성
T[] 제한 없음 사용은 쉽지만 최악 크기 예측이 어려움
T[N] 정확히 N Vector·Matrix처럼 크기가 고정된 Data
T[<=N] 0~N 최대 Memory·Wire 크기를 제한 가능
string<=N 최대 N 긴 입력으로 인한 Memory 증가 제한

Bounded Type을 쓴다고 자동으로 Real-time이나 Zero Copy가 보장되는 것은 아닙니다. 다만 최대 크기를 정의해 Memory 계획, Validation과 일부 Middleware 최적화 가능성을 높입니다. 제한값은 임의로 작게 정하지 말고 실제 Hardware 최대값과 향후 확장을 반영합니다.


6. Constant와 Default Value

uint8 MODE_OPEN=0
uint8 MODE_CLOSE=1
uint8 MODE_HOLD=2

uint8 mode MODE_HOLD
float32 speed 0.5
  • Constant는 TYPE UPPER_CASE_NAME=value이며 전송되는 Instance Field가 아닙니다.
  • Default Value는 Field 뒤에 값을 적으며 새 객체를 만들 때 초기값으로 사용됩니다.

Constant 추가는 일반적으로 Serialized Field Layout을 바꾸지 않지만 Consumer Code의 의미와 상태 정책에는 영향을 줄 수 있습니다. Default 변경도 Wire Layout은 같아도 값을 명시하지 않는 Publisher의 동작을 바꿀 수 있으므로 Release Note와 Test가 필요합니다.

상수로 State를 표현할 때는 다음을 지킵니다.

  • 기존 숫자의 의미를 재사용하지 않습니다.
  • Unknown 또는 Unspecified 상태를 설계합니다.
  • 수신자가 모르는 새 값을 받았을 때 안전한 동작을 정의합니다.
  • State가 복잡해지면 별도 State Message나 Transition 규칙을 문서화합니다.

7. 실습 Interface 세트 설계

이 강의에서는 Interface 전용 Package my_robot_interfaces를 만듭니다.

my_robot_interfaces/
├── CMakeLists.txt
├── package.xml
├── msg/
│   ├── GripperCommand.msg
│   └── GripperState.msg
├── srv/
│   └── SetGripperMode.srv
└── action/
    └── GraspObject.action

GripperCommand.msg:

# Gripper가 적용할 목표 명령
uint8 MODE_OPEN=0
uint8 MODE_CLOSE=1
uint8 MODE_HOLD=2

std_msgs/Header header          # Command 생성 시각과 gripper frame
uint8 mode                      # MODE_* 중 하나
float32 position                # 0.0 닫힘 ~ 1.0 열림
float32 max_effort              # [N], 0이면 Controller 기본 제한
float32 speed                   # 0.0 ~ 1.0 정규화 속도
bool wait_for_contact
string<=64 command_id

GripperState.msg:

# 측정·추정된 Gripper 상태
std_msgs/Header header
float32 position                # 0.0 ~ 1.0
float32 effort                  # [N]
bool moving
bool contact_detected
uint16 fault_code               # 0이면 Fault 없음
string<=128 fault_message

Command와 State를 분리하면 누가 무엇을 요구했는지와 Hardware가 실제로 무엇을 하고 있는지 비교할 수 있습니다.


8. srv 설계: Request와 Response

Service File은 --- 구분선 하나를 사용합니다.

# srv/SetGripperMode.srv
uint8 MODE_OPEN=0
uint8 MODE_CLOSE=1
uint8 MODE_HOLD=2

uint8 mode
float32 max_effort              # [N]
---
bool accepted
uint16 error_code
string<=128 message

위는 Request, 아래는 Response입니다. Service는 짧고 제한된 시간에 끝나는 작업에 적합합니다. 긴 Motion, 진행률과 Cancel이 필요한 작업을 Service Callback에서 Blocking하면 Executor와 안전 반응을 막을 수 있으므로 Action을 사용합니다.

Custom Service를 만들기 전에 std_srvs/srv/Trigger, SetBool, Empty가 의미에 맞는지 확인합니다. 단, Mode와 힘 제한처럼 Domain 의미가 필요하면 Custom Service가 더 명확합니다.


9. action 설계: Goal, Result와 Feedback

Action File은 두 개의 ---로 세 부분을 나눕니다. 순서는 Goal → Result → Feedback입니다.

# action/GraspObject.action
# Goal
string<=64 object_id
geometry_msgs/PoseStamped target_pose
float32 max_effort              # [N]
float32 timeout                 # [s]
---
# Result
bool success
uint16 error_code
string<=128 message
my_robot_interfaces/GripperState final_state
---
# Feedback
uint8 phase
float32 progress                # 0.0 ~ 1.0
my_robot_interfaces/GripperState current_state

Action은 Goal 수락·거절, 실행 중 Feedback, Result와 Cancel Protocol을 위한 내부 Interface도 생성합니다. 그렇다고 Actuator가 자동으로 안전 정지하는 것은 아닙니다. Server는 Cancel 확인 주기, Hardware 정지 확인, Timeout과 새 Goal 정책을 구현해야 합니다.

ServiceAction
RequestGoal
ResponseResult
짧은 작업긴 작업
중간 Feedback 없음반복 Feedback
표준 Cancel 없음Cancel Protocol 있음

10. Interface 전용 Package 만들기

ROS 2의 표준적인 Interface 생성은 ament_cmake와 rosidl Generator를 사용합니다. Python Node가 ament_python이라면 Interface를 별도 ament_cmake Package로 분리하는 방식이 가장 명확합니다.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake my_robot_interfaces
mkdir -p my_robot_interfaces/msg
mkdir -p my_robot_interfaces/srv
mkdir -p my_robot_interfaces/action

Interface 전용 Package의 장점:

  • 실행 Node와 독립적으로 가볍게 배포할 수 있습니다.
  • Python·C++ Package가 같은 계약을 공유합니다.
  • 의존 순서와 Version 관리가 분명합니다.
  • 관제 PC가 Hardware Driver 없이 Type만 설치할 수 있습니다.

Package 이름은 조직 규칙에 따라 _interfaces 또는 _msgs를 사용하되 한 Project 안에서는 일관되게 유지합니다.


11. CMakeLists.txt 표준 설정

cmake_minimum_required(VERSION 3.8)
project(my_robot_interfaces)

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

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/GripperCommand.msg"
  "msg/GripperState.msg"
  "srv/SetGripperMode.srv"
  "action/GraspObject.action"
  DEPENDENCIES
    std_msgs
    geometry_msgs
)

ament_export_dependencies(rosidl_default_runtime)
ament_package()

점검 항목:

  • 새 File을 rosidl_generate_interfaces() 목록에 넣었는가?
  • 다른 Package Type을 Field로 썼다면 find_package() 했는가?
  • 같은 Package를 DEPENDENCIES에도 선언했는가?
  • File 이름과 실제 대소문자가 일치하는가?
  • ament_package()는 마지막에 있는가?

Action에서 자기 Package의 Message를 참조하는 것은 같은 Generation Set에서 처리할 수 있습니다. 외부 Package만 DEPENDENCIES에 추가합니다.


12. package.xml 표준 설정

<?xml version="1.0"?>
<package format="3">
  <name>my_robot_interfaces</name>
  <version>0.1.0</version>
  <description>My robot public ROS 2 interfaces</description>
  <maintainer email="robot@example.com">Robot Team</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>
  <depend>geometry_msgs</depend>

  <member_of_group>rosidl_interface_packages</member_of_group>

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

rosidl_default_generators는 Build 시 Code를 만들고, rosidl_default_runtime은 생성된 Interface를 실행 환경에서 사용하는 데 필요합니다. 외부 Type 의존성을 빠뜨리면 Clean 환경과 다른 Computer에서 Build 순서 또는 Type Support 문제가 나타날 수 있습니다.


13. Build, Source와 CLI 검증

cd ~/ros2_ws

# Dependency 확인
rosdep install --from-paths src --ignore-src -r -y

# Interface와 의존 Package까지 Build
colcon build --packages-up-to my_robot_interfaces

# 현재 Shell에 새 Install Overlay 적용
source install/setup.bash

# 생성 결과 확인
ros2 interface package my_robot_interfaces
ros2 interface show my_robot_interfaces/msg/GripperCommand
ros2 interface show my_robot_interfaces/srv/SetGripperMode
ros2 interface show my_robot_interfaces/action/GraspObject
ros2 interface proto my_robot_interfaces/msg/GripperCommand

Build 성공만으로 현재 Shell이 새 Type을 아는 것은 아닙니다. source install/setup.bash 이후 ros2 interface show까지 성공해야 1차 검증이 끝납니다.

Build 산출물 흐름:

01Interface 원본
02rosidl Generator
03install의 Python·C++ Type Support
04setup.bash로 Prefix 등록
05CLI와 Consumer Package에서 발견

14. Python Publisher 예제

Consumer Python Package의 package.xml에는 다음 의존성을 선언합니다.

<depend>rclpy</depend>
<depend>my_robot_interfaces</depend>
import rclpy
from rclpy.node import Node

from my_robot_interfaces.msg import GripperCommand


class GripperCommandPublisher(Node):
    def __init__(self):
        super().__init__('gripper_command_publisher')
        self.publisher = self.create_publisher(
            GripperCommand,
            '/gripper/command',
            10,
        )
        self.timer = self.create_timer(1.0, self.publish_command)
        self.sequence = 0

    def publish_command(self):
        self.sequence += 1
        message = GripperCommand()
        message.header.stamp = self.get_clock().now().to_msg()
        message.header.frame_id = 'gripper_base'
        message.mode = GripperCommand.MODE_CLOSE
        message.position = 0.2
        message.max_effort = 15.0
        message.speed = 0.4
        message.wait_for_contact = True
        message.command_id = f'grasp-{self.sequence:04d}'
        self.publisher.publish(message)
        self.get_logger().info(f'published {message.command_id}')


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


if __name__ == '__main__':
    main()

실제 Actuator가 연결된 Command Topic에 Test Data를 바로 넣지 않습니다. 먼저 /test/gripper/command 같은 격리 Topic, Simulation 또는 Driver Disable 상태에서 Type과 값 범위를 검증합니다.


15. Python Subscriber와 Validation

Interface Type이 맞는다고 값이 안전한 것은 아닙니다. 수신 경계에서 Range, Enum, Timestamp와 Frame을 검증합니다.

import math

import rclpy
from rclpy.node import Node

from my_robot_interfaces.msg import GripperCommand


class SafeGripperReceiver(Node):
    VALID_MODES = {
        GripperCommand.MODE_OPEN,
        GripperCommand.MODE_CLOSE,
        GripperCommand.MODE_HOLD,
    }

    def __init__(self):
        super().__init__('safe_gripper_receiver')
        self.subscription = self.create_subscription(
            GripperCommand,
            '/gripper/command',
            self.on_command,
            10,
        )

    def on_command(self, message):
        if message.mode not in self.VALID_MODES:
            self.get_logger().error('unknown mode; command rejected')
            return
        if not math.isfinite(message.position) or not 0.0 <= message.position <= 1.0:
            self.get_logger().error('position outside [0.0, 1.0]')
            return
        if not math.isfinite(message.max_effort) or message.max_effort < 0.0:
            self.get_logger().error('invalid max_effort')
            return
        if message.header.frame_id != 'gripper_base':
            self.get_logger().error('unexpected frame_id')
            return
        self.get_logger().info(f'accepted {message.command_id}')


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

Hardware Driver에서는 추가로 Command Age, Sequence 중복, Rate Limit, Mode Transition, Watchdog와 E-stop 상태를 확인해야 합니다.


16. C++에서 생성 Type 사용

CMake Consumer Package:

find_package(rclcpp REQUIRED)
find_package(my_robot_interfaces REQUIRED)

add_executable(gripper_sender src/gripper_sender.cpp)
ament_target_dependencies(gripper_sender
  rclcpp
  my_robot_interfaces
)

install(TARGETS gripper_sender
  DESTINATION lib/${PROJECT_NAME}
)
#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "my_robot_interfaces/msg/gripper_command.hpp"

using namespace std::chrono_literals;

class GripperSender : public rclcpp::Node
{
public:
  GripperSender() : Node("gripper_sender")
  {
    publisher_ = create_publisher<
      my_robot_interfaces::msg::GripperCommand>("/test/gripper/command", 10);
    timer_ = create_wall_timer(1s, [this]() { publish_once(); });
  }

private:
  void publish_once()
  {
    my_robot_interfaces::msg::GripperCommand message;
    message.header.stamp = now();
    message.header.frame_id = "gripper_base";
    message.mode = my_robot_interfaces::msg::GripperCommand::MODE_HOLD;
    message.position = 0.5F;
    message.max_effort = 8.0F;
    message.speed = 0.25F;
    message.command_id = "cpp-hold";
    publisher_->publish(message);
  }

  rclcpp::Publisher<my_robot_interfaces::msg::GripperCommand>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<GripperSender>());
  rclcpp::shutdown();
  return 0;
}

17. CLI로 Code 없이 시험하기

ros2 topic pub --once /test/gripper/command \
  my_robot_interfaces/msg/GripperCommand \
  "{header: {frame_id: gripper_base}, mode: 1, position: 0.2, max_effort: 10.0, speed: 0.3, wait_for_contact: true, command_id: cli-test}"

ros2 topic echo /test/gripper/command --once
ros2 topic info /test/gripper/command --verbose

ros2 service call /test/gripper/set_mode \
  my_robot_interfaces/srv/SetGripperMode \
  "{mode: 2, max_effort: 5.0}"

ros2 action send_goal /test/gripper/grasp \
  my_robot_interfaces/action/GraspObject \
  "{object_id: box_01, max_effort: 12.0, timeout: 5.0}" \
  --feedback

CLI YAML은 Type과 Field 이름 확인에는 좋지만 실제 Robot Command의 안전을 대신하지 않습니다. Motion 가능성이 있으면 Driver Disable·물리 격리·낮은 제한값·Watchdog·E-stop을 먼저 확인합니다.


18. IDL과 rosidl 생성 Pipeline

.msg, .srv, .action은 ROS 개발자가 읽기 쉬운 문법입니다. Build 과정에서 rosidl Adapter·Parser·Generator가 중간 표현과 언어별 Code·Type Support를 만듭니다.

01msg·srv·action — ROS 개발자가 작성하는 계약
원본
02rosidl_adapter·parser — 정의 해석과 중간 표현
문법·Type 해석
03rosidl_generator_c·cpp·py — 언어별 Type 생성
언어 API
04rosidl_typesupport — RMW가 사용할 Type Support 연결
Middleware 경계
05DDS/RMW Backend — Serialization과 전송
Wire Data

IDL을 직접 사용하는 고급 기능도 있지만 처음에는 .msg/.srv/.action과 공식 Generator 흐름을 사용합니다. 생성 File을 직접 수정하면 다음 Build에서 덮어써지므로 원본 Interface와 Generator 설정만 Version 관리합니다.


19. Versioning과 호환성

Interface Definition은 분산된 모든 Component가 같은 계약을 사용해야 합니다. Field 추가·삭제·순서·Type·이름 변경을 “작은 수정”으로 보지 않습니다.

변경 Wire Layout 영향 운영 대응
Comment 수정 없음 문서 Review
Constant 추가 일반적으로 Instance Layout 없음 Consumer의 Unknown 상태 처리 Test
Default 변경 Layout 없음, 생성 동작 변화 값을 생략하는 Publisher Test
Field 추가·삭제 있음 새 Version Type과 동시 Migration 검토
Field 순서 변경 있음 하지 않음
Field Type·Bound 변경 있음 새 Type·Topic·Bridge 검토
Field 이름 변경 Code·Metadata 영향 Major 변경으로 취급

서로 다른 Definition이 같은 Type 이름을 사용하면 연결 거부, Type 불일치, Deserialization 실패 등 구현·Type Support에 따른 문제가 생길 수 있습니다. 안전하게 통신된다고 가정하지 않습니다.

권장 Migration:

  1. GripperCommandV2 또는 Version이 구분된 Package를 정의합니다.
  2. V1↔V2 변환 Node를 만듭니다.
  3. 일정 기간 Dual Publish 또는 Bridge를 운영합니다.
  4. Bag·Dashboard·Robot Fleet Consumer를 순차 전환합니다.
  5. 사용량을 관찰한 뒤 V1을 제거합니다.

File 이름에 무조건 숫자를 붙이는 것보다 Package Release·Semantic Version과 조직 정책을 함께 사용합니다. 그러나 호환되지 않는 두 계약을 같은 이름으로 조용히 교체하는 것보다는 명시적 Version이 안전합니다.


20. Bag, Bridge와 외부 System

Bag에는 Serialized Data와 Type 정보가 연결됩니다. 현재 Workspace에 해당 Interface Package가 없거나 Definition이 달라지면 재생·분석 Tool이 Data를 해석하지 못할 수 있습니다.

현장 기록 시 보존할 것:

  • Interface Repository Commit·Release
  • ROS Distribution과 Package Version
  • Bag Metadata와 QoS Override
  • 변환 Node Version
  • Message Definition 또는 Type Description Export

외부 REST·CAN·PLC·Cloud Schema와 연결할 때는 ROS Type을 그대로 모든 곳에 강요하지 말고 명시적 Adapter를 둡니다. 단위, Range, Endianness, Enum과 Missing Value 변환을 Test합니다.


21. CI와 품질 검증

colcon build --packages-up-to my_robot_interfaces
colcon test --packages-select my_robot_interfaces
colcon test-result --verbose

ros2 interface show my_robot_interfaces/msg/GripperCommand

CI 권장 검사:

  • Clean 환경에서 Build되는가?
  • package.xml 의존성이 완전한가?
  • 모든 Interface File이 CMake 목록에 있는가?
  • Comment에 단위·범위·시간·Frame 의미가 있는가?
  • Bounded Field의 최대값 Test가 있는가?
  • Python·C++ Consumer가 생성 Type을 Compile·Import하는가?
  • CLI와 Serialization Round-trip이 되는가?
  • 이전 Release와 호환성 차이를 Review했는가?
  • 실제 Robot 안전 범위를 벗어난 Test Data를 거부하는가?

Generated Code 자체보다 Definition과 Consumer 경계 행동을 Test합니다.


22. 오류 진단표

증상 우선 확인 해결 방향
ros2 interface show에 없음 CMake 목록·Build·Source File 등록 후 Build·새 Shell Source
Python ModuleNotFoundError Overlay·Python Path 올바른 Workspace Source와 Package 설치 확인
C++ Header 없음 Consumer Dependency·Build 순서 find_package·ament_target_dependencies 확인
외부 Type을 못 찾음 CMake DEPENDENCIES·package.xml 양쪽 의존 선언 추가
새 Field가 Code에 없음 이전 Install Overlay 의존 Package 재Build·Process 재시작
다른 Robot만 통신 실패 배포 Version·Type Definition Fleet 전체 Package Version 비교
CLI YAML 오류 interface show/proto Field 구조와 Array 길이 확인
값은 오지만 위험 Range·Age·Frame Validation 수신 경계와 Driver Safety 강화
# 어느 Package Prefix를 보고 있는가
ros2 pkg prefix my_robot_interfaces

# 현재 Overlay 순서
printenv AMENT_PREFIX_PATH | tr ':' '\n'

# 설치 산출물 확인
find install/my_robot_interfaces -maxdepth 4 -type f | sort

# 의존 관계 확인
ros2 pkg xml my_robot_interfaces
colcon list

build/, install/, log/ 전체 삭제는 마지막 수단입니다. 먼저 정확한 Package Prefix, CMake 등록과 Source 문제를 확인하고, 필요한 산출물만 대상으로 안전하게 Clean합니다.


23. 실제 Robot Interface Checklist

  • [ ] 표준 Interface를 먼저 조사했다.
  • [ ] Field 이름이 Domain 의미를 드러낸다.
  • [ ] 단위와 허용 범위를 Comment에 적었다.
  • [ ] 공간 Data의 Frame, Sensor Data의 측정 시각을 정의했다.
  • [ ] NaN, Unknown, Empty와 Fault 의미를 정의했다.
  • [ ] Sequence·String의 현실적인 최대 크기를 검토했다.
  • [ ] Command와 State를 분리했다.
  • [ ] Command Age, Timeout과 중복 처리 정책이 있다.
  • [ ] 새 Enum·상수를 모르는 Consumer의 행동을 정의했다.
  • [ ] Interface Version과 Fleet 동시 배포 계획이 있다.
  • [ ] Bag·Bridge·Dashboard Migration을 포함했다.
  • [ ] Python·C++·CLI·Clean CI에서 검증했다.
  • [ ] Hardware Driver가 잘못된 값을 독립적으로 거부한다.

24. 확인 퀴즈 15문항

답을 선택한 뒤 정답 확인을 누르세요. 정답과 해설은 제출 후에 표시됩니다.

1. Custom Interface를 만들기 전에 가장 먼저 할 일은?

2. RMW와 Application이 공유하는 Data 계약의 원본은?

3. int32[<=8] sensor_ids의 뜻은?

4. Constant와 Default Value의 차이는?

5. 공간 Sensor Data에 Header를 넣는 주된 이유는?

6. Service File의 구분선 수는?

7. Action File의 세 부분 순서는?

8. Python Node가 ament_python일 때 권장되는 Interface 구성은?

9. 새 msg File을 만들었는데 CLI에 나타나지 않는 가장 흔한 원인은?

10. 외부 geometry_msgs Type을 Field로 쓸 때 필요한 것은?

11. Field 추가를 안전한 Comment 수정처럼 취급하면 안 되는 이유는?

12. 실제 Robot Command Subscriber가 Type 외에 검증할 것은?

13. Generated Python·C++ File을 직접 수정하면 안 되는 이유는?

14. 호환되지 않는 V2로 Migration하는 안전한 방법은?

15. Interface Package CI에서 가장 중요한 검증은?


ROBOT GLOSSARY

용어 정리

전체 용어 찾아보기 →
ROS InterfaceROS 데이터 계약
Node 사이에서 교환할 Message, Service와 Action Data의 이름·Type·구조를 정의한 계약입니다.
msg메시지 정의
Topic에서 Publish·Subscribe할 Data Field 구조를 정의하는 ROS Interface File입니다.
srv서비스 정의
하나의 구분선으로 Request와 Response 구조를 정의하는 ROS Interface File입니다.
action액션 정의
두 구분선으로 Goal, Result와 Feedback 구조를 정의하는 ROS Interface File입니다.
rosidlROS 인터페이스 정의 체계
Interface를 해석해 언어별 Type과 Middleware Type Support를 생성하는 ROS 2 Package 집합입니다.
Type Support미들웨어 타입 지원
생성된 ROS Type을 특정 RMW·Middleware의 Serialization과 전송 기능에 연결하는 계층입니다.
IDL인터페이스 정의 언어
언어와 Platform에 독립적으로 Data Type과 Interface를 기술하는 표준 표현입니다.
Primitive Type기본 자료형
bool, int32, float64와 string처럼 Interface Field가 직접 사용할 수 있는 기본 Type입니다.
Fixed Array고정 길이 배열
Interface에서 원소 수가 항상 정확히 N개로 정해진 Array입니다.
Bounded Sequence상한 길이 시퀀스
원소 수가 0부터 지정한 최대 N개까지 허용되는 Sequence입니다.
Bounded String상한 길이 문자열
저장할 수 있는 최대 문자 길이가 Interface 계약에 정의된 String입니다.
Constant인터페이스 상수
생성 Code가 공유하지만 Message Instance의 Serialized Field로 전송되지 않는 이름 있는 값입니다.
Default Value필드 기본값
새 Message 객체를 만들 때 Field에 사용되는 초기값으로 Wire Field 구조 자체는 유지합니다.
std_msgs/Header표준 시간·프레임 헤더
측정 Timestamp와 Coordinate Frame ID를 함께 전달하는 표준 Message입니다.
ament_cmakeCMake 기반 ROS 빌드 유형
CMake와 ament 규약으로 Package를 Build·Install하며 표준 rosidl Interface 생성에 사용하는 Build Type입니다.
rosidl_default_generators기본 인터페이스 생성기 묶음
Build 시 ROS Interface에서 지원 언어와 Type Support 산출물을 생성하도록 제공되는 Package 집합입니다.
rosidl_default_runtime기본 인터페이스 실행 의존성
생성된 Interface Type을 Runtime Consumer가 사용할 때 필요한 의존성 묶음입니다.
Wire Compatibility전송 형식 호환성
서로 다른 Producer와 Consumer Version이 같은 Serialized Data 계약을 안전하게 해석할 수 있는 성질입니다.
Adapter Node인터페이스 변환 노드
서로 다른 Version이나 외부 Schema 사이에서 Field·단위·상태를 명시적으로 변환하는 Node입니다.
Semantic Versioning의미적 버전 관리
호환되지 않는 변경, 기능 추가와 수정의 의미를 Major·Minor·Patch Version으로 표현하는 규칙입니다.

연습 문제

  1. 자신의 Robot에서 Custom Type으로 만든 항목 중 표준 Type으로 대체할 수 있는 것을 조사하세요.
  2. GripperCommand의 Field별 단위·범위·Unknown 의미 표를 작성하세요.
  3. 무제한 Sequence를 Bounded Sequence로 바꿀 때 최대값을 정하는 근거를 작성하세요.
  4. my_robot_interfaces Package의 전체 Folder와 네 Interface File을 만드세요.
  5. CMakeLists.txt와 package.xml의 rosidl·외부 Type 의존성을 설명하세요.
  6. Build 후 interface show, proto와 Python Import를 검증하세요.
  7. 격리 Topic에 Python Publisher와 Safe Subscriber를 실행하세요.
  8. SetGripperMode Service Server·Client Test 계획을 작성하세요.
  9. GraspObject Action의 Goal 수락·Feedback·Cancel·Result 안전 조건을 정의하세요.
  10. 같은 Interface를 사용하는 C++ Consumer를 Build하세요.
  11. Field 추가가 필요한 V2 Migration과 변환 Node 구조를 설계하세요.
  12. 이전 Version으로 기록된 Bag을 보존·분석하는 절차를 작성하세요.
  13. 잘못된 Mode·NaN·오래된 Timestamp를 Subscriber가 거부하는 Test를 만드세요.
  14. Robot 열 대의 Interface Package Version을 검사하는 배포 Checklist를 작성하세요.
  15. Clean CI에서 Build·Test·Consumer Import·Compatibility Review를 자동화하세요.

참고 자료