여태까지 명령어랑 개념을 배웠다면 이제는 본격적으로 로봇개발에 기능을 만들 수 있게 해주는 publisher와 subscriber를 작성하는 예시를 이번 시간에 살펴볼 것이다.
목표:
C++로 publisher와 subscriber node를 작성하고 실행한다.
tutorial level: 입문
예상 소요시간
약 20분
예전에 topic 개념을 배웠듯이 ROS2는 node라는 곳에서 메세지를 publish(발행)하고 또다른 node에서 그 메세지를 subscribe(구독)하는 식으로 통신이 일어난다.
예를 들어 "talker"라는 node는 어떤 숫자의 메세지를 주기적으로 publish했고, "listener"라는 node는 그 숫자들을 주기적으로 subscribe했었다.
다음 포스트에 있는 실습을 마친 사람들이 이번 실습을 원활히 진행할 수 있다.
ros2라는 command를 사용하기 위해 ros2 workspace를 먼저 source해주자.
source /opt/ros/humble/setup.bash
지난 시간에 만들었던 ros2_ws/src directory로 이동해서 ros2 pkg create를 이용해서 패키지를 만들어주자.
CMake로 패키지를 만들 것이고, license로는 Apach-2.0, cpp_pubsub이라는 패키지 이름을 붙여줄 것이다.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
패키지를 생성했으면 패키지 안에 있는 src 폴더로 이동하자.
cd ~/ros2_ws/src/cpp_pubsub/src
쉽게 설명하기 위해 미리 작성된 코드를 다운 받고 코드를 살펴보는 식으로 진행하겠다.
먼저 ros2_ws/src/cpp_pubsub/src에서 다음과 같이 코드를 다운 받아주자.
wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp
그럼 다음과 같이 publisher_member_function.cpp라는 파일이 설치 된 것을 볼 수 있을 것이다.
"ubuntu software"이모티콘을 누른다.
code를 검색한다.
-visual code를 찾아 클릭한 후 install을 눌러준다.
-비밀번호를 입력하고 기다린다.
publsher_member_function.cpp를 다운 받았으면 방금 설치한 visual code로 열어준다.
cd ~/ros2_ws/src/cpp_pubsub/src
code . #현재 경로에 있는 파일 및 폴더를 visual code로 연다.
예시
그러면 다음과 같은 코드가 뜰 것이다.
<text 버전>
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
예제 코드를 하나씩 살펴보자.
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
먼저 C++ standard library들인 chrono, functional, memory, string을 include하고 있다.
그다음에 rclcpp/rclcpp.hpp를 include하게 되는데 이는 ROS2를 cpp로 개발할 때 가장 흔하게 필요한 헤더파일이다.
그 밑에 std_msgs/msg/string.hpp 는 ROS2 standard message중 string에 해당하는 message type을 사용하기 위한 헤더파일로써 publish할 때 string data type을 사용할 것이기 때문에 필요하다.
다음 using namcespace~는 std::chrono_literals를 매번 입력하는 수고를 덜기위해 작성해준다.
class MinimalPublisher : public rclcpp::Node
다음 살펴볼 라인은 rclcpp::Node에서 상속받아 MinimalPublisher라는 node class를 만들고 있다.
이 코드 밑에 사용되는 this라는 키워드는 이 node를 가리키게 된다.
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
다음 코드에서는 public constructor, MinimalPublisher를 정의하는데
이 constructor는 node이름을 "minimalpublisher"로 정하고, count를 0으로 초기화 한다.
그 후 "publisher_"를 string 타입의 메세지를 사용하도록 선언한다.
"topic"은 이 publihser가 메세지를 발행할 topic의 이름이고, topic이 가지고 있을 수 있는 message의 한계 개수는 10개로 지정한다.
다음 timer_라는 변수에 "timer_callback"함수를 선언한다. timer 함수는 보통 주기적으로 어떤 함수를 실행하고 싶을 때 사용하는데 그 주기는 지금 500ms(1초에 2번)으로 설정되어있으며, timer_callback함수에 정의된 기능을 실행하게 될 것이다.
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
위의 코드는 timer_callback()함수가 어떤 기능을 하는지 말해준다.
먼저 string타입의 메세지를 정의한후 그 메세지의 data에 "Hello, world"와 현재 카운트된 숫자를 조합해 저장한다.
그다음 terminal에 publish될 내용이 뭔지 알수 있도록 log를 띄우는 RCLCPP_INFO를 사용하고, 마지막으로 미리 정의된 publisher가 방금 작성한 message를 publish하게 된다.
위의 코드를 살펴보면서 timer, publisher, count_들이 멤버변수로 사용됐는데 이는 다음과 같이 하단에 정의 되어있다.
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
class의 생성자(constructor), 및 멤버 변수, 함수들이 정의 됐으면 이 클래스를 main함수에서 생성하여 기능하도록하는 코드를 다음과 같이 작성할 수 있다.
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
main함수에서는 rclcpp::init을 통해 ROS2 를 초기화 하고, rclcpp::spin으로 MinimalPublisher class에 정의된 publisher코드를 실행 시킨다.
rclcpp::shutdown()은 중간에 프로그램이 종료하라는 명령이 들어오면(ctrl+c입력등) ROS2를 종료하고 0값을 return하고 프로그램을 종료한다.
우리가 작성한 C++ node를 build하기 위해선 CMakeLists.txt와 package.xml을 적절히 수정해야한다.
먼저 pakcage.xml을 visual code로 열어주자.
그 다음, description, maintainer, license를 적절히 입력해주고(빌드랑은 상관 없는 부분)
다음과 같이 dependency를 입력해준다.
<depend>rclcpp</depend>
<depend>std_msgs</depend>
이는 우리 package는 rclcpp와 std_msgs를 dependecy로 갖는다는 말이다.
이제 CMakeLists.txt를 열어서 rclcpp와 std_msgs 패키지를 찾는 문구를 다음과 같이 find_package(ament_cmake REQUIRED) 밑에 넣어준다.
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
이는 패키지를 빌드할 때 이 두 패키지를 찾으라는 의미이다.
그다음 우리가 작성한 publisher_member_function.cpp의 executable을 추가하고 이 executable은 rclcpp std_msgs에 의존성을 요구한다는 문구를 다음과 같이 추가한다. executable이름은 talker로 한다.
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
이제 install(TARGETS ...)문구를 작성하여 ros2 run 명령어로 해당 executable을 실행할 수 있게 해준다.
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
최종 CMakeLists.txt는 다음과 같다.
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# Default to C++14
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_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
이제 패키지를 빌드 할 수 있지만 subscriber까지 작성하고 빌드하도록하자.
빠른 설명을 위해 먼저 subscriber node code를 다운받자.
cd ~/ros2_ws/src/cpp_pubsub/src
wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp
그러면 ls를 했을 때 다음과 같은 subscriber_member_function.cpp가 생긴 것을 확인 할 수 있을 것이다.
visual code로 subscriber_member_function.cpp를 열면 다음과 같은 코드를 볼 수 있다.
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
코드를 하나씩 살펴보자.
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
먼저 이전에 살펴봤던 것처럼 생성자를 통해서 "minimal_subscriber"라는 이름을 갖는 node를 생성하게 한다.
생성자는 subscription을 string type을 subscript하게 정의하고 이 메세지는 "topic"이라는 이름을 갖는 topic에서 읽어온다.
subscribe는 10개의 대기 메세지를 갖을 수 있으며, topic에서 message가 도착하면 topic_callback에 정의된 함수를 실행할 것이다.
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
그렇다면 topic_callback에 대한 정의가 필요한데, message가 들어오면
어떤 메세지를 들었는지에 대한 메세지를 RCLCPP_INFO를 통해 출력하도록 코드를 작성했다.
또한 subscription_ 멤버 변수에 필요한 타입을 rclcpp::Subscription을 이용해 정의한다.
main함수에서는 publisher node에서와 마찬가지고 node를 생성하고 이를 spin으로 돌아가게 만든다.
publisher node를 작성했을 때 처럼 executable에 대한 구문과 install 구문을 넣어주어야한다.
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
executable 이름은 listener이고, 빌드할 때 rclcpp std_msgs를 필요로한다.
빌드하기전에 항상 package.xml에 필요한 dependency를 설치하는 습관을 들이면 좋다. 이는 rosdep을 통해 쉽게 할 수 있다.
cd ~/ros2_ws
rosdep install -i --from-path src --rosdistro humble -y
그다음 colcon build해준다. cpp_pubsub만 빌드하면 되므로 pakcage-select tag를 추가해준다.
colcon build --packages-select cpp_pubsub
새로운 터미널을 열어 작업환경을 소스하고 talker node를 실행해준다.
cd ~/ros2_ws
. install/setup.bash
ros2 run cpp_pubsub talker
다음과 같은 메세지들이 출력되면 성공!
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
또 다른 터미널을 실행하여 listener node를 실행해준다.
cd ~/ros2_ws
. install/setup.bash
ros2 run cpp_pubsub listener
다음과 같이 publisher가 발행한 메세지를 받아 출력할 것이다.
[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"
여태까지 간단한 publisher와 subscriber에 대한 코드 작성 예시를 살펴봤다.
정말 단순하지만 timer callback이나 topic callback에 우리가 원하는 기능을 구현함으로써 유용하게 사용될 수 있는 개념이다.
예를 들어 카메라 데이터가 발행되면(publisher) 사람을 인식해라(subscriber)라는 기능을 하는 node들도 오늘 배운 개념을 이용해서 작성할 수 있다.
이렇게 활용성이 크기 때문에 로봇개발할 때 가장 빈번하게 사용되는 개념이기도 하다.
다음시간에는 좀 더 특수한 기능을 갖는 service server와 client를 작성하는 방법에 대해 배워볼 것이다.
오늘도 여기까지 온 스스로를 칭찬해주자!
질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!