[ROS2] Hello World 토픽
1. rclpy 버전
1. 패키지 생성
$ cd ~/test/src/
$ ros2 pkg create rclpy_pkg --build-type ament_python --dependencies rclpy std_msgs
2. 파이썬 패키지 설정 파일(setup.py)
패키지 설정 파일(setup.py)에서 주의할 점은 entry_points 옵션의 console_scripts 키를 사용한 실행 파일 설정이다. 예를 들어 helloworld_publisher와 helloworld_subscriber 콘솔 스크립트는 각각 rclpy_pkg.helloworld_publisher 모듈과 rclpy_pkg.helloworld_subscriber 모듈의 main 함수를 호출하게 된다. 해당 설정을 통해 ros2 run 또는 ros2 launch 명령어로 해당 스크립트를 실행시킬 수 있다.
from setuptools import setup
package_name = 'rclpy_pkg'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='jh',
maintainer_email='ahdtld54@korea.ac.kr',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'helloworld_publisher = rclpy_pkg.helloworld_publisher:main',
'helloworld_subscriber = rclpy_pkg.helloworld_subscriber:main',
],
},
)
3. Publisher 노드 작성(helloworld_publisher.py)
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String
# 이 노드의 메인 클라스는 HelloworldPublisher 이고 Node 클래스를 상속해 사용할 예정
class HelloworldPublisher(Node):
def __init__(self):
# 클래스 생성자 정의로 super().__init__('helloworld_publisher')를 이용해 부모 클래스(Node)의 생성자를 호출하고 노드 이름을 helloworld_publisher로 지정
super().__init__('helloworld_publisher')
# 퍼블리셔의 QoS 설정을 위하여 QoSProfile을 호출하고 기본 depth를 10으로 설정
# 이는 통신 상태가 원활하지 못하거나 예기치 못한 문제가 발생할 경우를 대비해 퍼블리시할 데이터를 버퍼에 10개까지 저장하는 설정이다.
qos_profile = QoSProfile(depth=10)
# Node 클래스의 create_publisher 함수를 이용해 helloworld_publisher를 설정
# 매개변수로 토픽 메시지 타입은 String, 토픽 이름은 helloworld, QoS는 qos_profile
self.helloworld_publisher = self.create_publisher(String, 'helloworld', qos_profile)
# Node 클래스의 create_timer 함수를 이용해 콜백 함수를 실행
# 첫 번째 매개벼수 timer_period_sec를 1로 설정하면 1초마다 지정한 콜백함수가 실행.
# 1초마다 publish_helloworld_msg를 실행한다.
self.timer = self.create_timer(1, self.publish_helloworld_msg)
# count는 콜백 함수에 사용되는 카운터 값이다.
self.count = 0
# 앞에서 지정한 publish_helloworld_msg 콜백 함수
def publish_helloworld_msg(self):
# 퍼블리시할 메시지는 String 타입으로 선언, 실제 데이터는 msg.data 변수에 저장
msg = String()
msg.data = 'Hello World: {0}'.format(self.count)
self.helloworld_publisher.publish(msg)
# get_logger는 프로그래밍에서 흔히 사용되는 print 출력 함수와 동일
self.get_logger().info('Published message: {0}'.format(msg.data))
self.count += 1
def main(args=None):
#rclpy.init를 이용해 초기화
rclpy.init(args=args)
# HelloworldPublisher 클래스를 node 변수로 생성
node = HelloworldPublisher()
try:
# 생성한 노느를 spin 시켜 지정된 콜백 함수가 실행될 수 있도록 함
rclpy.spin(node)
# 종료(Ctrl + c)와 같은 인터럽트 시그널 예외 상황에서는 node를 소멸시키고 rclpy.shutdown 함수로 노드를 종료
except KeyboardInterrupt:
node.get_logger().info('Keyboard Interrupt (SIGINT)')
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4. subscriber 노드 작성(helloworld_subscriber.py)
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile
from std_msgs.msg import String
class HelloworldSubscriber(Node):
# 클래스 생성자 정의로 super().__init__('helloworld_subscriber')를 이용해 부모 클래스(Node)의 생성자를 호출하고 노드 이름을 helloworld_subscriber로 지정
def __init__(self):
super().__init__('Helloworld_subscriber')
qos_profile = QoSProfile(depth=10)
# 토픽 메시지 타입은 String, 토픽 이름은 helloworld, 콜백 함수는 subscribe_topic_message, QoS는 qos_profile로 설정
self.helloworld_subscriber = self.create_subscription(
String,
'helloworld',
self.subscribe_topic_message,
qos_profile)
def subscribe_topic_message(self, msg):
self.get_logger().info('Received message: {0}'.format(msg.data))
def main(args=None):
rclpy.init(args=args)
node = HelloworldSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
node.get_logger().info('Keyboard Interrupt (SIGINT)')
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
두 노드가 통신하기 위해서 토픽(topic) 메시지를 전달하는 핵심 코드는 다음과 같다.
- Publisher
# 매개변수로 토픽 메시지 타입은 String, 토픽 이름은 helloworld, QoS는 qos_profile
self.helloworld_publisher = self.create_publisher(String, 'helloworld', qos_profile)
- Subscriber
# 토픽 메시지 타입은 String, 토픽 이름은 helloworld, 콜백 함수는 subscribe_topic_message, QoS는 qos_profile로 설정 self.helloworld_subscriber = self.create_subscription( String, 'helloworld', self.subscribe_topic_message, qos_profile)
코드를 쓰다가 퍼블리시 파일에서 토픽의 이름을 ‘helloword’ 라고 잘못 썼더니 다음과 같이 publisher 노드에서는 ‘helloword’ 토픽 메시지를 전달하고, subscriber 노드에서는 ‘helloworld’ 토픽 메시지를 subscribe 하는 문제가 발생했다.
오타를 수정해주니 해결되었다.
2. rclcpp 버전
1. 패키지 생성
$ cd ~/test/src/
$ ros2 pkg create rclcpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs
2. 빌드 설정 파일(CMakeLists.txt)
빌드 설정 파일에서 주의할 점은 의존성 패키지의 설정과 빌드 및 설치 관련 설정이다. 이 패키지에서는 빌드 시에 필요한 ament_cmake 패키지, 클라이언트 라이브러리 rclcpp 패키지, 메시지 인터페이스 std_msgs 패키지가 의존성 패키지로 사용됨을 명시하였다. 그리고 helloworld_publisher와 helloworld_subscriber 실행 파일의 빌드 및 설치를 위한 설정이 포함되어 있다.
# Set minimum required version of cmake, project name and compile options
cmake_minimum_required(VERSION 3.5)
project(my_first_ros_rclcpp_pkg)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
# Build
add_executable(helloworld_publisher src/helloworld_publisher.cpp)
ament_target_dependencies(helloworld_publisher rclcpp std_msgs)
add_executable(helloworld_subscriber src/helloworld_subscriber.cpp)
ament_target_dependencies(helloworld_subscriber rclcpp std_msgs)
# Install
install(TARGETS
helloworld_publisher
helloworld_subscriber
DESTINATION lib/${PROJECT_NAME})
# Test
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
endif()
# Macro for ament package
ament_package()
3. Publisher 노드 작성(helloworld_publisher.cpp)
// std 계열의 헤더를 선언
#include <chrono>
#include <functional>
#include <memory>
#include <string>
// rclcpp의 Node 클래스를 사용하기 위한 rclcpp.hpp 헤더 파일
#include "rclcpp/rclcpp.hpp"
// 퍼블리시하는 메시지 타입인 String 메시지 인터페이스를 사용하기 위한 string.hpp 헤더 파일
#include "std_msgs/msg/string.hpp"
// 추후에 "500ms", "1s"와 같이 시간을 가식성이 높은 문자로 표현하기 위해 namespace를 사용할 수 있도록 선언
using namespace std::chrono_literals;
// 이 노드의 메인 클래스는 HelloworldPublisher로 rclcpp의 Node 클래스를 상속해 사용할 예정
class HelloworldPublisher : public rclcpp::Node
{
public:
// 클래스의 생성자 정의로 부모 클래스(Node)의 생성자를 호출하고 노드 이름을 helloworld_publisher로 지정, count_ 변수는 0으로 초기화
HelloworldPublisher()
: Node("helloworld_publisher"), count_(0)
{
auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
// Node 클래스의 create_publisher 함수를 이용하여 퍼블리셔를 설정하고 있다.
// 매개변수의 토픽 메시지 타입은 String, 토픽 이름은 helloworld, QoS는 qos_profile로 설정했다.
helloworld_publisher_ = this->create_publisher<std_msgs::msg::String>(
"helloworld", qos_profile);
// Node 클래스의 create_wall_timer 함수를 이용해 콜백 함수를 실행
// 첫 번째 매개변수는 주기이며 1s로 설정하면 1초마다 지정한 콜백 함수(publish_helloworld_msg)가 실행된다.
timer_ = this->create_wall_timer(
1s, std::bind(&HelloworldPublisher::publish_helloworld_msg, this));
}
private:
void publish_helloworld_msg()
{
auto msg = std_msgs::msg::String();
msg.data = "Hello World: " + std::to_string(count_++);
// RCLCPP_INFO 함수는 터미널 창에 출력하는 함수로,
// 로거의 종류에 따라 RCLCPP_DEBUG, RCLCPP_INFO, RCLCPP_WARN, RCLCPP_ERROR, RCLCPP_FATAL을 선택해 사용하면 됨
// RCLCPP_XXXX 계열의 함수는 프로그래밍에서 흔히 사용되는 print 함수라고 생각하면 이해하기 쉬움
RCLCPP_INFO(this->get_logger(), "Published message: '%s'", msg.data.c_str());
helloworld_publisher_->publish(msg);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr helloworld_publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
// 초기화
rclcpp::init(argc, argv);
// HelloworldPublisher 클래스를 node 변수로 생성
auto node = std::make_shared<HelloworldPublisher>();
// rclcpp::spin 함수를 이용하여 생성한 노드를 spin시켜 지정된 콜백 함수가 실행될 수 있도록 함
rclcpp::spin(node);
// 종료(Ctrl+c)와 같은 인터럽트 시그널 예외 상황에서는 rclcpp::shutdown 함수로 노드를 소멸하고 프로세스 종료
rclcpp::shutdown();
return 0;
}
4. Subscriber 노드 작성(helloworld_subscriber.cpp)
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
// placeholders 클래스는 bind 함수의 대체자 역할을 하기 위하여 _1로 선언
using std::placeholders::_1;
class HelloworldSubscriber : public rclcpp::Node
{
public:
HelloworldSubscriber()
: Node("Helloworld_subscriber")
{
auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
helloworld_subscriber_ = this->create_subscription<std_msgs::msg::String>(
"helloworld",
qos_profile,
std::bind(&HelloworldSubscriber::subscribe_topic_message, this, _1));
}
private:
void subscribe_topic_message(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "Received message: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr helloworld_subscriber_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<HelloworldSubscriber>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
5. 빌드
$ cd ~/test
$ colcon build --symlink-install --packages-select rclcpp_pkg
Starting >>> rclcpp_pkg
Finished <<< rclcpp_pkg [5.60s]
Summary: 1 package finished [7.42s]
특정 패키지의 첫 빌드 후에는 환경설정 파일을 불러와서 실행 가능한 패키지의 노드 설정을 해줘야 빌드된 노드를 실행할 수 있다.
$ . ~/test/install/local_setup.bash