다음 예시를 생각해보자.
친구 A, B, C가 있다고하자. A는 C에게 하고 싶은 말이 있다. 그렇다면 A가 C에게 말하는게 빠를까 아니면 A가 B한테 말한 후 B가 C에게 말하는게 빠를까?
당연히 A가 C에게 말하는게 더 빠를 것이다. 단순히 생각해보면 A가 B에게 말하고 B가 다시 C에게 말한다면 전달 내용을 B가 기억(저장)해야하기 때문에 B를 통해 전달하는 것이 더 느리다.
ROS2는 A publisher와 C subscriber node가 서로 다른 processor에서 동작한다면 이처럼 A와 C는 B를 통해서 대화하고 있다고 보면 된다.
그렇다면 A와 C가 서로 직접접으로 대화 할 수 있게 하는 방법은 없을까?
당연히 똑똑한 ROS2 개발자들은 이를 염두해두고 ROS2를 개발했기 때문에 이렇게 빠르게 정보를 전달할 수 있는 방법이 존재한다.
바로 두개의 node를 동일한 processor내에서 처리함으로써 정보 교환시 따로 복사하지 않고 바로 전달할 수 있게 하는 방법이다.
이때 새로운 용어들이 등장하는데 이해하고 나면 어렵지 않으니 차근 차근 배워보자.
component: 제일 처음 배울 용어는 component이다.
component는 우리가 일전에 배운 node를 가리키는 말이다. 일반적인 node와 다르게 다른 node와 같은 processor내에 묶이는 구성원으로 사용될 수 있으므로 구성 요소(component)라는 이름이 붙였다.
composable node: 뭔가 복잡해보이지만 component와 같은 의미를 갖는다. component node를 여러개 사용하여 밑에 배울 composition(구성)을 만들 것인데 이때 composition에 사용가능한 node라고 해서 composable(구성가능한)이라는 이름이 붙었다.
composition: 한개의 processor내에 composable node(=component)들로 이루어진 형태를 composition(구성)이라고한다. 한개의 processor내(한 composition)에서 node들간 통신을 하는 경우 데이터 복사과정이 없기 때문에 좀 더 빠르고 효율적인 통신이 가능하다.
요약해보자면,
composition은 composable한 node(=component)로 구성된다.
이 composition을 만들게 되면 node들간 빠른 통신이 가능하다.
"아니, component(=composable node), composition같은 어려운 이름을 써서 왜 더 헷갈리게 할까?" 라는 생각이 드는가?
그럴 수 있다. 그런데 로봇개발은 상대적으로 우리가사용하는 PC보다 느린 컴퓨터에서 개발되므로 효율성이 굉장히 중요하다.
또한 로봇에서는 이미지 같은 방대한 데이터가 빠른 주기로 처리되야한다. 그렇기 때문에 이런 이미지를 불필요하게 복사하는 행위는 효율성을 떨어뜨릴 수 있다.
따라서 composition같은 개념을 도입해서 로봇이 좀 더 효율적으로 동작할 수 있게 해야한다.
항상 해왔듯이 composition 구현 실습을 위한 작업공간과 패키지를 하나 만들자.
mkdir -p composition_ws/src
cd composition_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 simple_composition
cd simple_compositon
code .
아직 우리는 component(=composable node)를 만들기 위해 어떤 dependency가 있는지 잘 모르겠으므로 패키지 만들때 따로 dependency를 추가해주지 않았다.
패키지내부 경로로 이동해서 visual code를 열어주자.
먼저 우리 지난시간에 배웠던 talker를 component로 만들어볼 것이다.
지난 번 talker와 기능적인 면은 똑같지만 내부적으로 동작하는 방식이 다를 것이다.
패키지 내 src폴더에 다음과 같이 talker_component.cpp라고 파일을 만들어준다.
talker_component.cpp에 다음과 같은 코드를 작성한다.
// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <chrono>
#include <iostream>
#include <memory>
#include <utility>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
namespace composition
{
// Create a Talker "component" that subclasses the generic rclcpp::Node base class.
// Components get built into shared libraries and as such do not write their own main functions.
// The process using the component's shared library will instantiate the class as a ROS node.
class Talker : public rclcpp::Node
{
public:
Talker(const rclcpp::NodeOptions & options)
: Node("talker", options), count_(0)
{
// Create a publisher of "std_mgs/String" messages on the "chatter" topic.
pub_ = create_publisher<std_msgs::msg::String>("chatter", 10);
// Use a timer to schedule periodic message publishing.
timer_ = create_wall_timer(1s, std::bind(&Talker::on_timer, this));
}
void on_timer()
{
auto msg = std::make_unique<std_msgs::msg::String>();
msg->data = "Hello World: " + std::to_string(++count_);
RCLCPP_INFO(this->get_logger(), "Our talker publishing: '%s'", msg->data.c_str());
std::flush(std::cout);
// Put the message into a queue to be processed by the middleware.
// This call is non-blocking.
pub_->publish(std::move(msg));
}
private:
size_t count_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
} // namespace composition
#include "rclcpp_components/register_node_macro.hpp"
// Register the component with class_loader.
// This acts as a sort of entry point, allowing the component to be discoverable when its library
// is being loaded into a running process.
RCLCPP_COMPONENTS_REGISTER_NODE(composition::Talker)
뭔가 어마어마하게 달라진거 같지만 주석을 제외하고 저번에 배운 talker.cpp코드와 상당히 유사한거 같다.
하나하나 살펴보자.
다음과 같은 코드의 일부를 CMakeLists.txt에 적절히 복사 붙여넣기 한다.
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(std_msgs REQUIRED)
add_library(talker_component SHARED
src/talker_component.cpp)
ament_target_dependencies(talker_component
rclcpp
rclcpp_components
std_msgs)
rclcpp_components_register_nodes(talker_component "composition::Talker")
예시)
CMakeLists.txt를 작성했으면 package.xml도 적절히 수정해주자.
다음과 같은 코드를 적절히 복사 붙여넣는다.
<build_depend>rclcpp</build_depend>
<build_depend>rclcpp_components</build_depend>
<build_depend>std_msgs</build_depend>
<exec_depend>launch_ros</exec_depend>
<exec_depend>rclcpp</exec_depend>
<exec_depend>rclcpp_components</exec_depend>
<exec_depend>std_msgs</exec_depend>
예시)
이대로 컴파일 해도 되지만 listener까지 작성하고 compile하도록 하자.
이제 listener를 component로 만들어보자.
기능은 일반적인 listener와 동일하고 내부적으로 통신한는 방식만 데이터 복사 없이 일어난다는 점이 다르다.
// Copyright 2016 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <iostream>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
namespace composition
{
class Listener : public rclcpp::Node
{
// Create a Listener "component" that subclasses the generic rclcpp::Node base class.
// Components get built into shared libraries and as such do not write their own main functions.
// The process using the component's shared library will instantiate the class as a ROS node.
public:
Listener(const rclcpp::NodeOptions & options)
: Node("listener", options)
{
// Create a callback function for when messages are received.
// Variations of this function also exist using, for example, UniquePtr for zero-copy transport.
auto callback =
[this](std_msgs::msg::String::ConstSharedPtr msg) -> void
{
RCLCPP_INFO(this->get_logger(), "Our listener heard: [%s]", msg->data.c_str());
// std::flush(std::cout);
};
// Create a subscription to the "chatter" topic which can be matched with one or more
// compatible ROS publishers.
// Note that not all publishers on the same topic with the same type will be compatible:
// they must have compatible Quality of Service policies.
sub_ = create_subscription<std_msgs::msg::String>("chatter", 10, callback);
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_;
};
} // namespace composition
#include "rclcpp_components/register_node_macro.hpp"
// Register the component with class_loader.
// This acts as a sort of entry point, allowing the component to be discoverable when its library
// is being loaded into a running process.
RCLCPP_COMPONENTS_REGISTER_NODE(composition::Listener)
뭐가 많아 보인다. 하나하나 살펴보자.
talker_component처럼 CMakeLists.txt에 library와 component를 추가하는 코드를 작성해주자.
add_library(listener_component SHARED
src/listener_component.cpp)
ament_target_dependencies(listener_component
rclcpp
rclcpp_components
std_msgs)
rclcpp_components_register_nodes(listener_component "composition::Listener")
예시)
추가로 install 경로에 해당 library들이 추가 될 수 있게 함수를 다음과 같이 추가한다.
install(TARGETS
talker_component
listener_component
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
예시)
나중에 launch파일로 composition을 만들어서 실행하게 되면 .so 형태의 라이브러리 파일이 사용되는 것을 볼 수 있을 것이다.
listener_component는 talker_component와 같은 dependency를 가지고 있으므로 딱히 추가할 dependency는 없다.
자 이제 대망의 composition을 구성할 차례이다.
component들로 composition을 구성하는 방법은 여러 방법이 있지만 이번 실습에서는 가장 실요적인 launch파일로 composition을 구성하는 방법을 알아볼 것이다.
먼저 launch파일 경로를 만들어준다.
cd ~/composition_ws/src/simple_composition
mkdir launch
cd launch
code .
launch폴더 내부에 "simple_components_launch.py"를 만들어준다.
예시)
launch.py에 다음과 같은 코드를 작성한다.
# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Launch a talker and a listener in a component container."""
import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
"""Generate launch description with multiple components."""
container = ComposableNodeContainer(
name='my_container',
namespace='',
package='rclcpp_components',
executable='component_container',
composable_node_descriptions=[
ComposableNode(
package='simple_composition',
plugin='composition::Talker',
name='talker'),
ComposableNode(
package='simple_composition',
plugin='composition::Listener',
name='listener')
],
output='screen',
)
return launch.LaunchDescription([container])
코드를 살펴보자
이렇게 component로 추가해놓은 talker와 listener를 사용해서 launch파일에서 composition을 구성했다.
그럼 CMakeLists.txt에서 launch파일을 추가하고 compile해보자.
# Install launch files.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}
)
예시)
파일들의 변경사항을 저장했는지 확인하고 빌드 및 실행해보자.
cd ~/composition_ws/
colcon build
cd ~/composition_ws/
source install/setup.bash
ros2 launch simple_composition simple_components_launch.py
예시)
실행 예시를 보면 몇가지 주목 할만한 메세지들이 있다.
오늘 내용은 새로운 개념을 배워야 하기 때문에 살펴볼게 정말 많았다.
그러나 핵심은 간단하기 때문에 두려워하지말자!
<핵심요약>
요약해보니 6줄 밖에 안된다. 별거 없는 듯 하다.
앞으로 방대한 데이터의 교환이 필요할 때 composition을 통해 구현해보자.
대신 구현할 때 기본적인 composition구조를 갖추고 내부 상세 알고리즘을 작성하는 습관은 디버깅 지옥에 빠지는 것을 예방해주니 꼭 기억하도록 하자!
다음 시간에는 node 상태를 관찰하고 변경하는 lifecycle에 대해 배워볼 예정이다!
처음엔 어렵지만 많이 유용하니 꼭 공부해주길 바란다~!
질문하고 싶거나 인공지능 & 로봇 개발에 대해 다뤄줬으면 하는 주제를 댓글로 남겨주기 바란다~!
문의메일: irobou0915@gmail.com
오픈톡 문의: https://open.kakao.com/o/sXMqcQAf
참고 문헌:
오늘도 잘 보고 갑니다!!!