.. Cover Letter

공부#Robotics#자율주행/(ROS2) 기초

(ROS2 기초)7. ROS2에서 C++으로 기초 코딩하기

BrainKimDu 2023. 5. 11. 23:20

참고도서 1. ROS 2로 시작하는 로봇 프로그래밍. 표윤석 .  루비페이퍼 . 2021 
참고도서 2. ROS2 혼자공부하는 로봇SW 직접 만들고 코딩하자 . 민형기 .  잇플 . 2022
참고자료 1. PinkWink(민형기)님의 ROS2 강의자료
참고자료 2. ROS2 humble documeation
ROS2를 복습하는 김에 제대로 다시 한 번 정리하고 넘어가고 싶어서 이 시리즈를 작성합니다. 
이 글은 Turtlebot3 패키지를 제대로 이해하고, 다양한 센서를 부착하면서 활용하는 것을 목표로 합니다.
또한 글이 끝날 때 ROS와 관련된 문제를 하나 만들어서 직접 풀어보도록 합니다.
이번 시간에는 ROS C++에서 코딩하는 기초를 다룹니다.


ROS 프로그래밍 기초 (C++)

이전시간에 만든 ros_study_cpp 패키지에서 진행합니다.

아 혹시나 이전시간에 bashrc에 패키지 환경을 불러오는 명령어의 숏컷을 등록을 했습니다. 

alias ros_study="humble; source ~/ros_study/install/local_setup.bash; echo \"ros_study workspace is activated.\""

다음을 bashrc에 등록하고 터미널을 재시작하면 됩니다. 파이썬과 크게 다르지 않게 내용만 C++로 달라지는 것이기 때문에 차이점 위주로 설명을 진행합니다. 

다음처럼 실행하면 VScode를 열 수 있습니다.

cpp 패키지는 다음과 같은 구성을 가지고 있습니다. 우선 my_node.cpp를 하나 만들어 보도록 하겠습니다. CPP 패키지에서 소스 파일은 src폴더 내부에 만들게 됩니다.
다음처럼 코드를 짜도록 합시다.

#include<iostream>

int main()
{
  std::cout << "hello world";
}

다음처럼 파일을 만들고 나면 해당 파일을 실행가능하게 만들어주어야합니다. 우리는 CMakeList.txt파일을 건들어야 합니다. 다음의 파일을 확인해보시면 아무것도 없어서 어디를 건들어야할지 모릅니다.

저희는 처음에 그냥 패키지를 만들면서 다른 옵션(노드를 추가한다거나)을 건들지는 않았었습니다. 그래서 CMakeList에는 적혀있지 않아서 직접 적어주어야합니다.

add_executable(my_node src/my_node.cpp)

다음의 코드를 추가합니다.  이렇게되면 my_node.cpp를 실행가능하게 됩니다.

다음처럼 위치는 크게 상관없는 것으로 보입니다. 편한 곳에 추가를 합니다. 또한 다음 코드를 추가하여 터미널 상에서 실행이 가능하게 만듭니다. 이 경우는 파이썬에서 entry_point를 지정하는 것과 같습니다.

install(TARGETS
  my_node
  DESTINATION lib/${PROJECT_NAME})

이 코드가 없다면 터미널에서 my_node를 찾을 수 없습니다. 그래서 추가를 해주고
colcon build하여 my_node를 실행하면

colcon build
ros_study 
ros2 run ros_study_cpp my_node

다음처럼 ros run을 통해서 실행되는 것을 확인할 수 있습니다. 여기서 우리는 노드를 직접 만든 것은 아니지만 실행가능하게 만들고, 터미널 상에서 ros2 run 명령어로 코드를 실행하기 위해서 CMakeList.txt를 건드는 과정을 따라해보았습니다.
 
C++로 topic을 publish, subscribe해보기
파이썬에서 해봤던 내용을 다시 C++로 해보는 과정입니다. 해당 코드를 이해하기 위해서는 C++의 코딩 스타일과 객체지향 프로그래밍 방법을 어느정도 이해하고 있어야합니다.  코드의 출처는 documentation의 C++부분입니다. 
https://docs.ros.org/en/foxy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html

 

Writing a simple publisher and subscriber (C++) — ROS 2 Documentation: Foxy  documentation

You're reading the documentation for an older, but still supported, version of ROS 2. For information on the latest version, please have a look at Humble. Writing a simple publisher and subscriber (C++) Goal: Create and run a publisher and subscriber node

docs.ros.org

Pulisher node 부터 작성을 해보도록 합시다. 도큐먼트에서 다음의 코드를 가져옵시다.

#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;

먼저 include 와 namespace 구문입니다. 우선 C++의 std 계열의 헤더를 선언했습니다. 그 후에 ROS의 헤더인 rclcpp헤더를 선언하고, String 타입의 메시지 인터페이스를 include하였습니다. 
chrono_literals는 추후에 500ms, 1s 와 같이 시간을 가식성이 높은 문자로 표현하기 위해 namespace를 사용할 수 있도록 선언했습니다. 
 

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

다음은 노드를 선언하는 부분입니다. 
먼저 MinimalPublisher 라는 노드를 선언하기 위해 클래스를 선언합니다. 여기서 rclcpp::Node를 상속 받음으로써 ros의 노드로 선언하게 됩니다. 
생성자에 대한 접근 지정은 public으로 설정하고 생성자에 이름은 minimal_pulisher로 지정했습니다 그 뒤에 count_(0)는 count_ 라는 변수는 0으로 초기화를 시켰다. 여기서 count_ 처럼 뒤에는 _가 붙는 이유는 C++의 ROS 코드 스타일을 따랐기 때문이다.

더보기

C++ 의 ROS 스타일
C++의 코드 스타일은 Google C++ Style Guide를 사용하고 있으며, ROS의 특성에 따라 일부를 수정해서 사용한다.
1. 기본규칙 : C++ 14 Standard를 준수한다.
2. 라인 길이 : 최대 100문자
3. 이름 규칙
- CamelCased, snake_case, ALL_CAPITALS만을 사용한다.
CamelCased : 타입, 클래스, 구조체, 열거형
snake_case : 파일, 패키지, 인터페이스, 네임스페이스, 변수, 함수, 메소드
ALL_CAPITALS  : 상수, 메크로
- 소스 파일은 cpp 확장자를 사용한다.
- 헤더파일은 hpp 확장자를 사용한다.
- 전역변수를 반드시 사용해야할 경우 접두어 g_ 를 붙인다.
- 클래스 맴버 변수는 마지막줄에 밑줄 _을 붙인다.
4. 공백문자 대 탭
- 기본 들여쓰기는 공백 문자 2개를 사용한다 (탭을 사용하지 않는다.)
- Class 의 접근지정자는 들여쓰기 하지 않는다. 
5. 괄호
- if, else, do, while, for 구문에 괄호를 사용한다.
- 괄호를 사용하는 올바른 방법은 다음을 참고한다.
먼저 C++에서의 코딩 방법이다.

int main(int argc, char **argv)
{
  if (condition){
      return 0;
    } else {
      return 1;
    }
}

if (this && that || both){

}

// 긴 조건문인 경우
if (
  this && that || both && this && that || both %% this && that || both
)
{

}

// 짧은 함수 호출문
call_func(foo, bar);

// 긴 함수 호출문
call_func(
  foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, 
  foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar, foo, bar);


// 매우 긴 함수 인수를 사용해야하는 경우, 가독성을 위해 개행
call_func(
  bang,
  fooooooooooooooooooooooooooooooooo,
  bar, bat);

ReturnType LongClassName::ReallyReallyReallyReallyLongFunctonName(
  Type par_name1,  //2 스페이스 들여쓰기
  Type par_name1,
  Type par_name1,)
{
  DoSomething();
}

MyClass::MyClass(int var)
: some_var_(var), // 첫 구문은 들여쓰기를 사용하지 않고, 이후부터는 스페이스 들여쓰기
  some_other_var_(var+1) 
{
  Dosomething();
}

6. 주석
- 문서 주석은 /** */를 사용한다.
- 구현 주석은 //을 사용한다.
7. 린터(Linters)
- C++ 코드 스타일의 자동 오류 검출을 위하여 ament_cppint, ament_uncrustify를 사용하고 정적 코드 분석이 필요한 경우 ament_cppcheck를 사용한다.
8. 기타
- Boost 라이브러리의 사용은 가능한 피하고 어쩔 수 없을 경우에만 사용한다.
- 포인터 구문은 char * c 처럼 사용하며 char* c나 char *c를 허용하지 않는다.
- 중첩 템플릿은 set<list<string>>처럼 사용한다. 

그 다음으로는 publisher_ 는 Node 클래스에 있는 create_publisher함수를 이용하여 퍼블러셔를 설정하고 있다. 매개변수는 std::msgs::msg::string 이라고 하는데, 결국  토픽의 메시지 타입을 뜻한다.
그 다음으로 timer_ 는 파이썬에서 보았듯이 일정시간마다 call_back함수를 작동시키기 위해 사용한다. Node 클래스의 create_wall_timer 함수를 이용해서 콜백함수를 실행시킨다. 첫번째로는 주기 500ms를 넣었고, 그 뒤에는 std namespace에 있는 bind라는 함수를 실행시키는 것인데 편하게 생각해서 MinimalPublisher 클래스(노드)의 timer_callback이라는 함수를 실행시키자는 의미이다.  뭔가 이해가 안되는 구문이라 chatgpt에 물어봤다. 
std::bind는 함수와 인자를 묶어서 나중에 호출할 수 있는 객체를 만드는 함수입니다. 여기서 묶이는 함수는 MinimalPublisher::timer_callback이며, 첫 번째 인자로 this가 전달됩니다. this가 첫 번째 인자로 전달되는 이유는 timer_callback이 MinimalPublisher 클래스의 멤버 함수이기 때문입니다. 즉, 이 함수는 MinimalPublisher 클래스의 인스턴스에서 호출되어야 합니다. this는 현재 MinimalPublisher 클래스의 인스턴스를 가리킵니다. std::bind 호출은 MinimalPublisher 클래스의 현재 인스턴스에서 timer_callback을 호출하는 새로운 호출 가능한 객체를 만듭니다. 이 새로운 호출 가능한 객체는 create_wall_timer 함수에 콜백 함수로 전달되어, 타이머에 의해 주기적으로 호출됩니다.
결국 콜백함수를 사용하기 위해 사용하는 구문이다. 일단 이정도만 이해하고 넘어가도록 합시다. this가 현재 MinimalPublisher의 객체를 가리키니까 이걸 callback으로 호출할때 this에 대한 MinimalPublisher를 실행한다는 뜻으로 받아들이면 될 것 같습니다.

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

그 다음은 call_back 함수를 까보겠습니다. 왜 private로 이 함수를 선언했을까? 간단하게 생각해서 call_back함수를 외부에서 사용하게 되면 안되니까 private로 선언을 했을 것 입니다.
auto message 메시지 형식은 auto로 지정을 했습니다. (자료형을 알아서 찾겠죠) 그래서 여기에 담을 메시지는 String을 담을 것이니 위에서 std_msgs::msg::String 형태의 메시지 타입으로 지정하고 data에 내용을 담았습니다. 
RCLCPP_INFO는 터미널 창에 출력하는 함수로 RCLCPP_DEBUG, RCLCPP_INFO, RCLCPP_WARN, RCLCPP_ERROR, RCLCPP_FATAL 을 선택해서 사용하면 됩니다. ROS에서 사용하는 print함수라고 생각하면 편합니다. 그래서 log를 남기고 publisher_ 를 발행하게 됩니다.
마지막으로 클래스에서 private 변수로 사용되는 timer_, helloworld_publisher_, count_ 를 선언했습니다. 여기서 timer_는 타이머를 사용하기 위해서 호출되었고, publisher_는 rclcpp의 publisher를 나타내는 포인터입니다. 
이러한 call_back함수는 member function, lambda, local function방법이 있습니다. 

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

다음은 메인함수에 대한 부분입니다. 간단하게 rclcpp를 초기화하고 MiniMalPublisher 노드를 제작하고 spin(실행)시키고, 노드가 종료되면 삭제시킵니다. 
 
그러면 이제 이 코드를 패키지에 넣고 실행해봅시다. src폴더 내부에 my_publisher.cpp라는 파일을 만들고 안에 코드를 넣읍시다.

지금 보시면 dependency문제가 발생합니다. rclcpp/rclcpp.hpp와 std_msgs/msg/string.hpp 파일을 찾지 못하고 있습니다. 이 문제는 package.xml을 수정하면 됩니다. 또한 CMakeList.txt에 패키지를 찾을 수 있게 해주어야합니다.

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

다음을 추가합니다.

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

그리고 지금 만든 파일을 CMakeList.txt에 추가해주도록 합시다.  

add_executable(my_publisher src/my_publisher.cpp)
ament_target_dependencies(my_publisher rclcpp std_msgs)

install(TARGETS
  my_publisher
  DESTINATION lib/${PROJECT_NAME})

이제 빌드하고 실행을 해보도록 합시다.

colcon build
ros_study
ros2 run ros_study_cpp my_publisher

잘 실행되고 있는 것을 확인할 수 있습니다.
 
 
이제 이를 받아보는 Subscriber를 작성해보도록 합시다.

#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::SharedPtr 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;
}

다음의 코드입니다. 코드는 도큐먼트에서 가져왔습니다. 

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

include와 namespace 구문을 나타내고, std::placeholders::_1; 은 placeholders 클래스에 bind 함수의 대체자 역할을 위해서 _1로 선언을 했다. 이 부분도 chatgpt에 물어보자. 
using std::placeholders::_1;은 C++11의 std::placeholders 라이브러리에서 제공하는 _1 placeholder를 사용하겠다는 선언입니다. 이 placeholder는 일반적으로 함수 객체를 생성할 때 인자를 부분적으로 바인딩(bind)할 때 사용됩니다. ROS에서는 이 placeholder를 콜백 함수에서 매개변수를 받을 때 사용합니다. 콜백 함수는 대개 특정 토픽 또는 서비스에서 수신된 데이터를 처리합니다. 그러나 이러한 데이터는 대개 콜백 함수의 인자로 전달됩니다. 때로는 콜백 함수의 인자 수를 미리 알 수 없는 경우도 있습니다. 이런 경우, _1 placeholder를 사용하여 콜백 함수에서 처리할 데이터의 위치를 나타낼 수 있습니다. _1은 첫 번째 매개변수를 의미하며, 필요에 따라 _2, _3, ...과 같은 추가적인 placeholder를 사용할 수도 있습니다.
어려운 이야기인거 같습니다. 그냥 토픽을 받을 때 사용한다는 것으로 일단 이해를 하고 넘어갑시다.

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

publisher의 구조와 동일하게 노드를 생성하는 모습입니다. subscription_ 멤버변수에 create_subscription 메소드를 통해서 메시지타입은 std_msgs::msg::String으로 하고 토픽의 이름도 정하고 콜백함수도 실행시키는 것을 볼 수 있습니다. 여기서 placeholder가 사용된 것을 알 수 있습니다. 

private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

 
다음은 topic_callback함수를 보시겠습니다. const std_msgs::msg::String::SharedPtr msg) 이부분에서 보실 부분은 뭐 어찌되었든 std_msgs::msg::String 메시지 타입으로 된   토픽 메시지를 받았으면 SharedPtr msg 형태로 해서 받겠다는 의미입니다. 그냥 const로 선언된 이유는 이 매게변수를 call_back함수가 수정하면 안되기 때문에 const를 붙였다고 이해하면 됩니다. 그리고 함수 외부에 const를 붙인 것은 이 함수가 클래스 내부의 변수를 변경하지 않음을 나타내는 예약어입니다. const로 선언된 코드는 함수 내에서 멤버 변수 값을 변경할 수 없게 만들어 오류를 사전에 방지하는 역할을 합니다.
그 뒤는 publisher와 같습니다. 이제 이를 코드 상으로 넣겠습니다. my_subscriber.cpp 파일을 하나 만들어 줍니다.

현재 이렇게 된 상태이고 CMakeList만 수정을 해줍니다.

add_executable(my_subscriber src/my_subscriber.cpp)
ament_target_dependencies(my_subscriber rclcpp std_msgs)

install(TARGETS
  my_publisher
  my_subscriber
  DESTINATION lib/${PROJECT_NAME})

다음처럼 수정합니다.

이제 빌드하고 두 파일을 실행해 봅시다.

colcon build
ros_study
ros2 run ros_study_cpp my_publisher
colcon build
ros_study
ros2 run ros_study_cpp my_subscriber

잘 실행됩니다. 
 

C++ 관련 문제 풀어보기 및 팁

my_publisher.cpp 에서 call_back함수는 현재 member function 형태로 선언되어 있습니다 이를 lambda 방식으로 수정해주세요.

#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(
        1s,
        [this]()->void
        {
          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);
        }
      );
    }

  private:
    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;
}

 다음처럼 수정해서 사용하는 것을 lambda 방식이라고 합니다. 다음과 같은 방식의 경우에는 콜백 함수가 간단할 때 사용하고, 콜백 함수가 복잡할 때는 멤버 함수 방식을 사용하는 것이 디버깅 상에서 편리하다고 합니다. 
 
QoS를 적용해주십쇼 버퍼에 10개까지 저장할 수 있게 해주세요
C++은 특이하게 따로 include하지 않아도 Qos를 바로 사용할 수 있는 것으로 보입니다.

public:
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic", qos_profile);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

다음처럼 코드를 수정하면 됩니다. QoS는 publish하는 쪽과 subscrbe 하는 쪽의 약속이 일치하지 않으면 서로 정보를 주고받을 수 없습니다. 그러니 subscribe도 수정을 해줍시다.

public:
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
      auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", qos_profile, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

잘 돌아갑니다.
 
 
turtlesim을 한 번 움직여 봅시다. 거북이가 간단하게 오른쪽으로 0.5초간 이동했다가 왼쪽으로 이동하는 topic을 발행해봅시다.
python에서 거북이를 움직이는 토픽이 /turtle1/cmd_vel 이라는 것과 그 메시지타입은 geometry_msgs/msg/Twist 이였습니다. 이를 C++로 변경해보는 것이 이 문제의 목표입니다. 혹시 topic을 알아내는 방법이 궁금하다면 이전 시간의 글의 맨 마지막을 참고합니다.
먼저 src에 turtlesim_move.cpp라는 파일을 만들고 C++의 뼈대가 되는 코드를 가져옵시다. my_publisher.cpp라는 파일에서 내용물을 지우면 됩니다.

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

#include "rclcpp/rclcpp.hpp"
#include <geometry_msgs/msg/twist.hpp>


using namespace std::chrono_literals;


class TurtlesimMove : public rclcpp::Node
{
  public:
    TurtlesimMove()
    : Node(""), count_(0)
    {
      auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
      publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("", qos_profile);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&TurtlesimMove::timer_callback, this));
    }

  private:
    void timer_callback()
    {
      auto message = 
      message
      RCLCPP_INFO(this->get_logger(), "");
      publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher_;
    size_t count_;
};

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

자 이제 내부를 채워넣어봅시다. 우리가 참고해야할 메시지 형식은 geometry_msgs/msg/Twist 입니다. 들고옵시다. 

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

#include "rclcpp/rclcpp.hpp"
#include <geometry_msgs/msg/twist.hpp>

토픽은 /turtle1/cmd_vel  이였습니다. 이에 해당하는 토픽을 발행해야합니다.

using namespace std::chrono_literals;


class TurtlesimMove : public rclcpp::Node
{
  public:
    TurtlesimMove()
    : Node("Turtlesim_move"), count_(0)
    {
      auto qos_profile = rclcpp::QoS(rclcpp::KeepLast(10));
      publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("/turtle1/cmd_vel", qos_profile);
      timer_ = this->create_wall_timer(
      500ms, std::bind(&TurtlesimMove::timer_callback, this));
    }

다음은 callback함수 부분을 수정합니다. 메시지를 가져와서 거기에다가 내용물을 채우고 날리면 됩니다.

private:
    void timer_callback()
    {
      auto message = geometry_msgs::msg::Twist();
      message.linear.x = 2.0;
      RCLCPP_INFO(this->get_logger(), "move");
      publisher_->publish(message);
    }
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher_;
    size_t count_;
};

그러면 Package.xml을 수정합니다.

<depend>geometry_msgs</depend>

다음을 추가합니다.
그리고 CMakeList.txt파일도 수정합니다.

find_package(geometry_msgs REQUIRED)

add_executable(turtlesim_move src/turtlesim_move.cpp)
ament_target_dependencies(turtlesim_move rclcpp geometry_msgs)

install(TARGETS
  turtlesim_move
  DESTINATION lib/${PROJECT_NAME})

 
그러면 빌드하고 거북이가 움직이는지 확인을 해봅시다.

움직이게 됩니다. 이 이후로는 코딩 싸움입니다. 

 private:
    void timer_callback()
    {
      if (direction_ == true){
        direction_ = false;
        auto message = geometry_msgs::msg::Twist();
        message.linear.x = 2.0;
        RCLCPP_INFO(this->get_logger(), "move-->");
        publisher_->publish(message);
      }
      else{
        direction_ = true;
        auto message = geometry_msgs::msg::Twist();
        message.linear.x = -2.0;
        RCLCPP_INFO(this->get_logger(), "<--move");
        publisher_->publish(message);
      }
      
    }

다음처럼 수정합니다.

거북이가 잘움직입니다. Python보다는 Cpp이 생각보다 까다로운 부분이 많은 것 같습니다.