ROS2에서 parameter는 어떻게 코드로 구현할 수 있을까?
오늘은 그 비밀에 대해 파헤쳐보자!
<목표>
C++로 class(node)내에 parameter를 정의하고 실행하는 실습을 진행해본다.
<예상 소요 시간>
약 20분
지난 시간의 NAVER LABS의 자율주행 로봇에서 수많은 node를 켤 수 있는 launch파일에 대해 설명했었다.(관련 영상)
이렇게 launch파일로 node를 실행할 때 node의 기능이 내가 정한 형태로 동작하게 하고 싶을 때가 있다.
이는 parameter라는 것으로 조절할 수 있는 경우가 많다.
예를 들어 자율주행 로봇의 이동속도나 장애물을 얼마나 멀리로부터 감지해서 피할 것인지 등이 parameter에 해당하며 이 외에도 수많은 파라미터를 로봇은 가지고 있다.
이번 시간에는 이런 parameter를 C++ class(node 생성을 담당하는 class)에서 구현해보고 launch파일에서 어떻게 설정해주는지 실습해보자.
'cpp_paramters'라는 이름을 갖고 rclcpp에 의존하는 package를 하나 생성하자.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_parameters --dependencies rclcpp
해당 패키지는 이번 시간에 우리가 parameter를 구현하고 설정하는데 사용할 예정이다.
지금 단계에서 필수는 아니지만 배포(누군가 내 패키지를 사용하게 뿌리는 행위)할 때를 대비해서 package.xml에 들어가서 description과 maintainer, license등을 다음과 같이 변경해주자.
<description>C++ parameter tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
maintainer의 이름은 적절히 본인의 email을 작성하면 된다.
이제 본격적으로 C++ node를 작성해보자.
먼저 cpp_parameters_node.cpp파일을 패키지내 src 경로에 생성한다.
cd ~/ros2_ws/src/cpp_parameters/src
gedit cpp_parameters_node.cpp
cpp_parameters_node.cpp에 다음과 같은 코드를 작성하도록 하자.
#include <chrono>
#include <functional>
#include <string>
#include <rclcpp/rclcpp.hpp>
using namespace std::chrono_literals;
class MinimalParam : public rclcpp::Node
{
public:
MinimalParam()
: Node("minimal_param_node")
{
this->declare_parameter("my_parameter", "world");
timer_ = this->create_wall_timer(
1000ms, std::bind(&MinimalParam::timer_callback, this));
}
void timer_callback()
{
std::string my_param = this->get_parameter("my_parameter").as_string();
RCLCPP_INFO(this->get_logger(), "Hello %s!", my_param.c_str());
std::vector<rclcpp::Parameter> all_new_parameters{rclcpp::Parameter("my_parameter", "world")};
this->set_parameters(all_new_parameters);
}
private:
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalParam>());
rclcpp::shutdown();
return 0;
}
(참고) chrono, functional, string은 C++표준 라이브러리에 속하는 헤더파일이다. rclcpp는 ROS2 패키지이며 rclcpp/rclcpp.hpp는 패키지 내 library의 헤더파일에 해당한다.
std::chrono_literals라는 namespace를 지정해서 1000ms(1000밀리 세컨드, 1초)등의 표현을 사용한다.
class 생성자(node를 생성을 담당하는 부분)에서 "declare_parameter"를 통해 "my_parameter"라는 파라미터를 기본값 "world"로 생성한다.
그다음 타이머를 생성하고 1000ms(1초)에 한번씩 주기적으로 timer_callback이라는 함수가 호출될 수 있도록 설정해준다. timer_callback은 하단에 따로 정의된다.
이제 timer_callback을 정의할 차례이다. parameter를 구현하는데 필수는 아니지만 이 실습에서는 paramter값으로 우리가 어떤 기능을 구현할 수 있는지 보여주기 위해 구현된 부분이라고 생각하면된다.
timer_callback에서는 다음과 같은 동작이 수행된다.
이때 의문이 들 수 있는데, "아니 이 행위를 왜하는거지?"라는 생각이 들 수 있다.
이따 실습에서 보겠지만 "my_paramter"라는 파라미터의 값이 혹시 사용자에 의해 외부에서 바뀔시 "world"라는 값으로 다시 바꿔버리기 위해 있는 코드이다.
실제 실습에서 확인하기로 하고 나머지 코드를 분석하자.
다음은 private로 timer를 선언해준다. 위 코드에서 사용하기 위함이다.
마지막으로 main함수에서 우리가 정의한 node가 실행될 수 있도록 객체화하는 동시에 spin으로 node가 켜져있을 수 있도록 코드를 작성한다.
Ctrl+C와 같은 중지 명령이 들어오면 node를 shutdown하고 0을 반환 후 프로그램을 종료한다.
조금 번거롭긴하지만 해놓으면 누군가 내가 생성한 paramter를 사용할 때 도움이 될 수 있는 코드 기능이 있다.
바로 내가 작성한 paramter가 어떤 역할을 담당하는지 묘사하는 설명을 달아 놓는 것이다.
예를 들어 다음과 같은 코드를 살펴보자.
// ...
class MinimalParam : public rclcpp::Node
{
public:
MinimalParam()
: Node("minimal_param_node")
{
auto param_desc = rcl_interfaces::msg::ParameterDescriptor{};
param_desc.description = "This parameter is mine!";
this->declare_parameter("my_parameter", "world", param_desc);
timer_ = this->create_wall_timer(
1000ms, std::bind(&MinimalParam::timer_callback, this));
}
위의 코드는 우리가 실습 2.에서 작성한 코드와 거의 유사하지만 자세히 보면
auto param_desc = rcl_interfaces::msg::ParameterDescriptor{};
param_desc.description = "This parameter is mine!";
해당 부분이 추가로 작성되어 있고 declare_paramter에서도 param_desc가 추가된 것을 볼 수 있다.
이는 "my_paramter"에 대한 설명을 추가한 것이라고 보면 된다.
이렇게 설명을 추가해놓으면 나중에 다음과 같은 명령어를 사용하여 터미널에서 파라미터의 설명을 확인할 수 있다.
ros2 param describe /minimal_param_node my_parameter
(예시)
코드 작성도 끝났겠다 이제 CMakeLists.txt 파일에 적절히 executable을 추가해주자.
add_executable(minimal_param_node src/cpp_parameters_node.cpp)
ament_target_dependencies(minimal_param_node rclcpp)
install(TARGETS
minimal_param_node
DESTINATION lib/${PROJECT_NAME}
)
이제 빌드하고 실행해보자
cd ~/ros2_ws
rosdep install -i --from-path src --rosdistro humble -y
cd ~/ros2_ws
colcon build --packages-select cpp_parameters
cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_parameters minimal_param_node
1초마다 my_parameter로부터 world를 읽어 log를 출려하는 것을 볼 수 있다. 단, log만 터미널에 보여줄 뿐 talker처럼 publish하는 것은 아니니 혼동하지 않도록 하자.
자 그러면 아까 timer_callback에서 world로 값을 왜 설정하게 했는지 이유와 관련된 추가 실습을 진행해보자.
터미널에서 node내 파라미터 값을 변경해주자.
먼저 ros2 param list 명령어를 통해 파라미터가 정상적으로 존재하는지 확인한다.
ros2 param list
결과
파라미터도 있겠다 이 값을 이번엔 earth로 변경해보자.
파라미터를 터미널에서 설정할 땐 다음과 같은 명령어를 사용한다. (상단의 파라미터 개념 포스트 및 영상 참고)
ros2 param set /minimal_param_node my_parameter earth
(예시)
그런데 한가지 이상한 것이 earth로 바꼈다가 다시 world로 돌아온 것을 볼 수 있다.
이는 아까 우리가 timer callback에서 사용자가 임의로 파라미터 값을 수정할 것을 대비해 원래 값인 world로 돌려놓도록 코딩했기 때문이다.
이렇게 parameter를 구현하고 코드내에서 해당 파라미터를 이용해서 작업을 할 수 있는 간단한 예시를 보여준다고 보면된다.
저번 launch파일을 작성하는 시간(관련 포스트 및 영상)에서도 배웠지만 다양한 기능을 하는 로봇인 경우엔 node수도 많고 해당 node들의 paramter양도 어마어마하다.
따라서 ros2 param set 명령어로 설정하는건 엄청난 시간이 필요하기 때문에 launch파일을 이용해서 한번에 미리 설정해놓은 값을 계속 사용할 수 있다.
먼저 launch 경로를 패키지 내에 만든다. 경로 내에 cpp_paramters_launch.py를 만든다.
cd ~/ros2_ws/src/cpp_paramters
mkdir launch
cd launch
gedit cpp_parameters_launch.py
다음과 같은 코드를 붙여넣자.
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="cpp_parameters",
executable="minimal_param_node",
name="custom_minimal_param_node",
output="screen",
emulate_tty=True,
parameters=[
{"my_parameter": "earth"}
]
)
])
간단히 코드를 분석해보자
parameters=[
{"my_parameter": "earth"}
]
이렇게 launch파일을 작성했으면 CMakeLists.txt에 가서 launch 폴더 내용물들이 share폴더에 설치되도록 설정해준다.
install(
DIRECTORY launch
DESTINATION share/${PROJECT_NAME}
)
colcon build하고 실행해준다.
cd ~/ros2_ws
colcon build --packages-select cpp_parameters
source install/setup.bash
ros2 launch cpp_parameters cpp_parameters_launch.py
launch파일에 의해 실행될 때 최초에 node가 생성될 때 earth로 세팅되었다가 timer_callback에서 다시 world로 값이 변경되는 것을 확인 할 수 있다.
오늘은 node내에서 parameter를 정의하고 그 값을 어떻게 획득 및 세팅하는지에 해에 알아보았다.
그 흐름을 정리하자면 다음과 같다.
구현 외에 파라미터 설정 방법은 두가지가 있었다.
로봇 개발에서 기능을 구현할 때(node를 만들때) 꼭 필요한 parameter개념이므로 익혀두면 두고두고 도움될 것이다.
문법을 하나하나 외운다기보다는 역시 흐름을 기억하고 항상 참고할 코드를 따로 준비해두도록 하자.
다음 시간에는 방대한 데이터를 처리해야하는 로봇개발에서 유용하게 사용될 수 있는 component(composable node)에 대한 기초 개념을 배워볼 예정이다.
질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!
ROS2를 독학하고 있는데 있어, 너무 큰 도움이 됩니다. 감사합니다.