지지난 시간과 지난시간에 topic의 publisher & subscriber 그리고 service의 server와 client를 직접 코드로 어떻게 구현하는지 알아봤다.
그런데 저번에는 이미 존재하는 msg와 srv파일을 사용했는데
로봇개발을 하다보면 나만의 msg와 srv를 만들고 싶을 때가 있다.
내가 직접 필요한 형태를 정의할 수 있을까?
물론 만들 수 있다! 오늘은 이 파일들을 어떻게 직접 만드는지에 대해 실습해보자.
<목표>
나만의 interface file(msg와 srv)파일을 정의하고 이를 사용하는 c++ node를 작성한다.
<예상 소요 시간>
20분
<동영상 tutorial>
유튜브 링크
저번에 service server와 client를 설명할 때 짜장면에 비유했던 것 처럼 고객(client)가 짜장면을 주문할 때 메뉴판을 보고 주문서에(srv파일에서 request) 적어서 주인(server)에게 service를 호출(call)한다고 했다.
모든 중국집이 같은 메뉴와 메뉴판을 사용한다면 미리 만들어진 메뉴판을 사용하면 되지만 새로운 메뉴가 추가 될 경우 새로운 메뉴판을 만들어야 할 것이다.
이렇게 로봇개발을 하다보면 누군가 만들어놓은(pre-defined) msg나 srv를 사용하는 경우가 많지만 가끔은 나만의 msg와 srv를 만들고 싶을 때가 있다.
예를 들어서 카메라 이미지를 subscribe해서 이미지내 사물들을 publish하는 Tesla의 자율주행 자동차가 있다고 하자.
이때 publish되는 msg에 사물 갯수, 사물의 이름을 담고 있는 list를 담고 싶은데 미리 정의된 msg가 없을 수 있다.
어떻게 할지 막막하지만 사실 ROS2는 자신만의 msg와 srv를 만들 수 있어서 그냥 내 나름대로 필요한 정보들을 정의해서 만들면된다.
이렇게 직접 만든 msg와 srv를 ROS2에서는 custom interfaces라고 한다.
이는 interface가 일반적으로 두개의 무언가를 연결하는 접점이라는 뜻인데 msg는 publisher와 subscriber를 srv는 service server와 client를 연결해준다점에 초점을 둬 이름을 지었다고 생각하면 기억하기 쉽다.
일단 interface를 정의하기 위해선 새로운 패키지가 필요하다.
준비물 세션에 링크된 publisher/subscriber(줄여서 pub/sub) 패키지와 service/client 패키지를 사용할거기 때문에 같은 ros2_ws/src에 새로운 패키지 "tutorial_interfaces"를 만들자.
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces
msg와 srv를 만들기 위해서는 "msg"와 "srv"라는 디렉토리가 필요하다.
이또한 만들어주자.
cd ~/ros2_ws/src/tutorial_interfaces
mkdir msg srv
(참고) mkdir msg srv 명령어는 각각 msg와 srv라는 디렉토리를 한번에 생성한다.
이제 나만의 interface(msg와 srv등)을 만들면 된다.
먼저 msg경로로 이동해서 "Num.msg"라는 파일명을 갖는 파일을 만든다.
cd ~/ros2_ws/src/tutorial_interfaces/msg
gedit Num.msg
그다음 Num.msg에 int64 타입의 num이라는 변수명을 갖는 타입을 복사 붙여 넣기해 정의해준다.
int64 num
(예시)
이 msg는 int64 type의 정보를 통신하는데 사용할 것이다.
추가로 Sphere.msg 파일도 생성해서 다음과 같이 center라는 변수와 radius변수를 정의한다.
cd ~/ros2_ws/src/tutorial_interfaces/msg
gedit Sphere.msg
geometry_msgs/Point center
float64 radius
(예시)
이 메세지도 나중에 원에 대한 정보를 통신하는데 사용할 예정이다.
다음으로는 srv파일들을 정의해보자.
srv 경로로 이동한 후 "AddThreeInts.srv"라는 파일을 만들자.
cd ~/ros2_ws/src/tutorial_interfaces/srv
gedit AddThreeInts.srv
그 후 파일 안은 다음과 같은 request와 response를 정의해준다.
int64 a
int64 b
int64 c
---
int64 sum
(예시)
여기서 a,b,c는 request로 쓰일 int타입 정보이고, sum은 response에 해당 된다.
이렇게 정의한 msg와 srv를 header 파일 형식으로 만들어 c++ node code를 작성할 때 사용하려면 CMakeLists.txt에 다음과 같은 문구들을 추가해야한다.
먼저 CMakeLists.txt를 visual code로 열어준다.
cd ~/ros2_ws/src/tutorial_interfaces
code .
그 다음 밑에 내용을 추가한다.
find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"msg/Sphere.msg"
"srv/AddThreeInts.srv"
DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)
(예시)
여기서 rosdil_generate_interfaces는 우리가 정의한 Num.msg, Sphere.msg, AddThreeInts.srv를 정의할 때 사용하는 CMake 함수이다.
참고로 rosdil은 ROS Interface Definition Language의 약자이다.
추가로 알아야 할 것은 msg와 srv를 정의할 때 사용한 int와 point 타입같은 것은 기존에 pre-defined된 타입이므로 int를 갖고 있는 msgs 패키지와 point를 갖고 있는 geometry 타입을 DEPENDENCIES에 추가해줘야한다.
그리고 rosdil_generate_interfaces함수에 들어가는 첫번째 인자(argument)는 우리 패키지의 project 이름과 반드시 같아야 하는데 이는 "${PROJECT NAME}"이라고 넣어줌으로써 항상 같은 것을 보장하게 할 수 있다.
interface를 만들 때 pakcage.xml파일에 추가해줘야할 것이 4개정도 있다.
먼저 다음을 package.xml에 package format과 /packge라고 있는 공간 사이에 추가하자. 추가해야 하는 위치를 잘 모르겠다면 아래 예시 이미지를 참고하자!
<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
(예시)
각각을 살펴보면
뭐라 설명이 복잡한데, 이 요소들 특히 rosil~ 어쩌고 하는 애들은 msg와 srv가 만들 때 필요한 것이고, geometry_msgs 같은 애들은 우리가 정의하는 srv와 msg에 따라 달라진다고 생각하면된다.
사실 다 외우고 다니는 건 아니고 특정 interface를 정의하는게 필요한 경우 그때 그때 찾아보면 되니 이해 안된다고 걱정하지 말고 용어만 눈에 익혀두자.
자 그럼 tutorial_interfaces를 빌드할 차례다.
다음과 같이 build해주자.
cd ~/ros2_ws
colcon build --packages-select tutorial_interfaces
새로운 터미널을 열어 우리 작업환경을 source해주고 정상적으로 msg와 srv가 정상적으로 생성됐는지 확인하자.
이때 ros2 interface show라는 명령어를 이용하면 생성여부를 확인 할 수 있다.
cd ~/ros2_ws
source install/setup.bash
ros2 interface show tutorial_interfaces/msg/Num
그러면 다음과 같은 문구가 출력 될 것이다.
그 다음으로는 srv파일을 확인해보자.
ros2 interface show tutorial_interfaces/srv/AddThreeInts
(결과 예시)
추가로 이 msg,srv에 해당하는 hpp파일은 다음과 같은 경로에 생성되어 있다.
interface가 성공적으로 만들어진 것도 확인 했겠다, 이제 msg를 위한 publisher, subscriber 그리고 srv를 위한 service server와 client를 만들어 직접 만든 msg와 srv는 어떻게 사용하는지 알아보자.
주의: 이부분부터는 publsher/subscriber, service server/client 구현 실습을 진행 했어야 할 수 있는 부분이다.
위에 보이는 그림과 같이 지난시간에 만든 cpp_pubsub packge를 visual code로 열어준 다음 publisher_member_function.cpp와 subscriber_member_function.cpp를 하단의 코드들로 대체 해준다.(적절히 복사 붙여넣기)
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // CHANGE
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = tutorial_interfaces::msg::Num(); // CHANGE
message.num = this->count_++; // CHANGE
RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'"); // CHANGE
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // CHANGE
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
달라진 점을 살펴보면 "//CHANGE"라고 써져 있는데 이전에 string type의 메세지를 publish하고 이용하는 부분들이 바뀐 것을 볼 수 있다.
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // CHANGE
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const tutorial_interfaces::msg::Num & msg) const // CHANGE
{
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'"); // CHANGE
}
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
subscriber코드에서도 비슷하게 변화된 부분이 CHANGE라고 표시되어 잇는데 "std_msgs/msg/string.hpp"에서 정의된 type들이 "tutorial_interfaces/msg/num.hpp"로 새롭게 정의한 num type을 사용하여 subscribe하고 데이터 처리는 하고 있다.
이제 CMakeLists.txt를 작성할 차례다.
다음과 같이 CMakeLists.txt에 복사 붙여넣기 하자. 어디다 복사 붙여넣기 해야할 지 모르는 사람은 아래 예시 이미지를 참고하자.
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
바꾸기 전 CMakeLists.txt
바뀐 후 CMakeLists.txt
바뀐 부분을 살펴보면 std_msgs 패키지에서 tutorial_interfaces로 변한 것을 확인할 수 있다.
마지막으로 pakcage.xml에 tutorial_interfaces dependency도 추가해주자.
<depend>tutorial_interfaces</depend>
그럼 colcon build하면된다! 참고로 파일을 변경했으면 꼭 ctrl+
S를 눌러 저장하자!
cd ~/ros2_ws
colcon build --packages-select cpp_pubsub
build가 완료 됐으면 새로운 terminal을 열어서 source해주자.
cd ~/ros2_ws
source install/setup.bash
그 후 publisher를 ros2 run으로 실행한다.
ros2 run cpp_pubsub talker
새 터미널을 열어서 비슷하게 listener도 실행해주자.
cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_pubsub listener
그러며 하단 이미지와 같이 msg를 publish하고 subscribe하는 log를 볼수 있다.
저번 시간에는 2개의 숫자를 더하는 코드를 짰었다.
이번에 만든 srv 파일 이름에서 유추할 수 있듯이 이번에는 3개의 숫자를 더하는 service server와 이를 호출하는 client 코드를 작성해볼 것이다.
먼저 pub/sub terminal에가서 Ctrl+C를 눌러 프로그램들을 종료해주자.
그 다음 ~/ros2_ws/src/cpp_srvcli 경로로 이동해서 visual code로 변형할 코드들을 열어주자.
cd ~/ros2_ws/src/cpp_srvcli
code .
(예시)
그 다음 add_two_ints_server.cpp를 변경해준다.
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <memory>
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request, // CHANGE
std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response) // CHANGE
{
response->sum = request->a + request->b + request->c; // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld", // CHANGE
request->a, request->b, request->c); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server"); // CHANGE
rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service = // CHANGE
node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // CHANGE
rclcpp::spin(node);
rclcpp::shutdown();
}
바뀐 부분은 CHANGE로 표시되어있는데 주로 example_interfaces/srv/add_two_ints.hpp 에서 add_two_ints가 사용되던 부분이 tutorial_interfaces/srv/add_three_ints.hpp의 add_three_ints가 사용되게끔 변경되었다. service 이름도 숫자 3개를 더하는 의미를 담도록 바뀌었다.
비슷하게 add_two_ints_client도 변형해준다.
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <chrono>
#include <cstdlib>
#include <memory>
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
if (argc != 4) { // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z"); // CHANGE
return 1;
}
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client"); // CHANGE
rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client = // CHANGE
node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints"); // CHANGE
auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>(); // CHANGE
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
request->c = atoll(argv[3]); // CHANGE
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = client->async_send_request(request);
// Wait for the result.
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints"); // CHANGE
}
rclcpp::shutdown();
return 0;
}
server와 비슷하게 바뀐 부분은 CHANGE로 표시되어있는데 주로 example_interfaces/srv/add_two_ints.hpp 에서 add_two_ints가 사용되던 부분이 tutorial_interfaces/srv/add_three_ints.hpp의 add_three_ints가 사용되게끔 변경되었다. service 이름도 숫자 3개를 더하는 의미를 담도록 바뀌었다.
그 다음 CMakeLists.txt를 변형해주자.
#...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp tutorial_interfaces) # CHANGE
add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client
rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
tutorial_interfaces 패키를 사용하도록 변경했으며 특이한점은 .cpp파일 자체 이름은 편의상 add_two_ints~.cpp로 유지하고 있다.
(예시)
마지막으로 package.xml에서 tutorial_interfaces를 dependency로 추가해준다.
<depend>tutorial_interfaces</depend>
(예시)
이제 colcon build할 차례다! build하기 전에 변경한 파일들을 다 제대로 저장했는지 Ctrl+S를 눌러서 한 번 더 확인한다.
cd ~/ros2_ws
colcon build --packages-select cpp_srvcli
새로운 터미널을 열어서 source하고 service server를 실행해준다.
cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_srvcli server
비슷하게 새로운 터미널을 열어서 source하고 service client를 실행해준다. 단, 이때 더할 인자 3개를 추가로 입력해준다.
cd ~/ros2_ws
source install/setup.bash
ros2 run cpp_srvcli client 2 3 1
(결과 예시)
그러면 이렇게 2,3,1을 더한 6이 반환되는 것을 볼 수 있다.
자 오늘은 이렇게 msg와 srv를 직접 만들고 pub/sub과 server/client 코드를 직접 작성해서 어떻게 사용하는지 또한 알아봤다.
만드는 과정을 요약하자면 다음과 같다.
어려워 보이지만 내 msg 또는 srv를 만들어야 할 때가 오면 어떻게 만드는지 그 순서를 보고 참고해서 하면되니 너무 외울려고 하진말고 그 흐름을 이해하도록 노력하자.
다음 시간에는 ros2 run을 대체할 수 있는 launch 파일 만드는 법에 대해 배워볼 예정이다.
오늘도 여기까지 온 스스로를 칭찬해주자!
질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!
문의메일: irobou0915@gmail.com
확실히 홈페이지보다 여기가 이해가 쉽게 되유