[ROS2] ROS2 프로그래밍 기초 - C++

HY K·2024년 9월 21일
0

ROS2

목록 보기
14/18

이번에는 C++ 기반 ROS2 프로그래밍의 기초에 대해서 알아보자.
참고한 링크는 다음과 같다.
https://cafe.naver.com/openrt/24451


C++ 기반 ROS2 프로그래밍 기초

패키지 생성

$ ros2 pkg create my_first_ros_rclcpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs

패키지 설정

package.xml

rclcpp 패키지기 때문에 cmake를 사용해서 빌드한다는 것을 알아야 한다.

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_first_ros_rclcpp_pkg</name>
  <version>0.1.0</version>
  <description>TODO: Package description</description>
  <maintainer email="kimhoyun@todo.todo">kimhoyun</maintainer>
  <license>Apache 2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

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

CMakeLists.txt

이 파일은 의존성 패키지의 설정과 빌드 및 설치 관련 설정이다. 한 단어로 줄이면, 빌드 설정 파일이라고 할 수 있다.

cmake_minimum_required(VERSION 3.8)
project(my_first_ros_rclcpp_pkg)

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)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

퍼블리셔 노드 작성하기

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;


class HelloworldPublisher : public rclcpp::Node{
public:
    HelloworldPublisher() : Node("helloworld_publisher"), count_(0){
        auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
        helloworld_publisher_ = this->create_publisher<std_msgs::msg::String>(
            "helloworld", qos_profile
        );
        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(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);
    auto node = std::make_shared<HelloworldPublisher>();
    rclcpp::spin(node);
    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;

이 부분의 경우, std 계열의 헤더들을 우선적으로 선언하고 있다.

  1. chrono : C++11부터 추가된 시간과 관련된 기능을 제공하는 헤더 파일
  2. functional : C++11부터 추가된 함수 객체를 다루기 위한 다양한 기능 제공, 람다 표현식, 표준 함수 객체(std::function)을 지원
  3. memory : 동적 메모리 관리를 위한 스마트 포인터 기능을 제공, std::shared_ptr, std::unique_ptr, std::weak_ptr과 같은 포인터 클래스가 포함됨
  4. string : C++ 표준 문자열 처리 라이브러리 기능 제공
  5. rclcpp/rclcpp.hpp : ROS2의 핵심 라이브러리인 rclcpp 포함
  6. string.hpp : std_msgs::msg의 String을 포함하는 헤더
  7. using namespace std::chrono_literals; : C++14에 추가된 기능으로, std::chrono의 시간 리터럴(예: 100ms, 1s 등)을 코드에서 직접 사용할 수 있도록 해주는 기능
class HelloworldPublisher : public rclcpp::Node

이 부분의 경우, Node 클래스를 상속해서 사용하겠다는 뜻이다.

HelloworldPublisher() : Node("helloworld_publisher"), count_(0){
        auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
        helloworld_publisher_ = this->create_publisher<std_msgs::msg::String>(
            "helloworld", qos_profile
        );
        timer_ = this->create_wall_timer(
            1s, std::bind(&HelloworldPublisher::publish_helloworld_msg, this));
    }

여기 있는 생성자의 기능을 정리하면 다음과 같다.

  1. 노드 생성자 호출 + 노드 이름 지정 + count_ 변수 초기화
  2. 퍼블리셔의 qoS 설정
  3. create_publisher 함수를 통해 퍼블리셔 설정
  4. create_wall_timer 함수를 이용해 주기를 1초로 지정한 콜백함수 실행
void publish_helloworld_msg(){
        auto msg = std_msgs::msg::String();
        msg.data = "Hello World : "+std::to_string(count_++);
        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_;

이 부분의 경우 publish_helloworld_msg 콜백 함수이다. 내용을 정리하면 다음과 같다.

  1. 퍼블리시 데이터 타입 및 실제 데이터 저장
  2. 로그를 통해 스크린에 표시
  3. private 변수로 사용되는 각종 변수들을 선언

💡 콜백 함수의 구현
콜백 함수 구현은 3가지 방식이 있다.

  • member function
  • lambda
  • local function
    이 중에서 주로 member function과 lambda 방식을 사용하게 된다.
int main(int argc, char *argv[]){
    rclcpp::init(argc, argv);
    auto node = std::make_shared<HelloworldPublisher>();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

마지막으로 main 함수를 통해서 초기화 및 노드 생성, 노드 실행, 노드 소멸, 프로세스 종료를 지정하고 있다.


서브스크라이버 노드 작성

#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

class HelloworldSubscriber : public rclcpp::Node
{
private:
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr helloworld_subscriber_;
    void subscribe_topic_message(const std_msgs::msg::String::SharedPtr msg) const{
        RCLCPP_INFO(this->get_logger(), "Received message: '%s'", msg->data.c_str());
    }
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)
        );
    }
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<HelloworldSubscriber>();
    rclcpp::spin(node);
    rclcpp::shutdown();
}

이것 역시 퍼블리셔 노드와 마찬가지로 하나씩 살펴보도록 하자.

#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

여기서 주목할 부분은, bind 함수의 대체자 역할을 위해서 placeholders 클래스를 "_1"로 선언한 것이다.

class HelloworldSubscriber : public rclcpp::Node
{
private:
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr helloworld_subscriber_;
    void subscribe_topic_message(const std_msgs::msg::String::SharedPtr msg) const{
        RCLCPP_INFO(this->get_logger(), "Received message: '%s'", msg->data.c_str());
    }

노드 클래스를 상속받아서 생성하고, 이후에 필요한 멤버 변수와 subscription 콜백 함수를 작성하였다.

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)
        );
    }

이 부분은 생성자를 통해 부모 클래스의 생성자를 호출하고 노드 이름을 지정하였다.
이후 QoS 설정 및 서브스크라이브 설정을 수행하고, 콜백 함수를 지정하였다.

마지막으로 main 함수를 통해 노드를 실행하고 종료한다.

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared<HelloworldSubscriber>();
    rclcpp::spin(node);
    rclcpp::shutdown();
}

빌드하기

$ colcon build # 전체 패키지 필드
$ colcon build --symlink-install --packages-select <pkg_name>
# 특정 패키지만 빌드
$ colcon build --symlink-install --packages-up-to <pkg_name>
# 의존성 패키지까지 한번에 빌드

$ source install/local_setup.bash

실행하기

$ ros2 run my_first_ros_rclcpp_pkg helloworld_publisher
$ ros2 run my_first_ros_rclcpp_pkg helloworld_subscriber
profile
로봇, 드론, SLAM, 제어 공학 초보

0개의 댓글