ROS2 humble 기준으로 작성하였습니다.
ROS에서 node란 실행가능한 가장 작은 단위를 의미한다고 저번 포스팅에서 언급하였다. 이 때 특정 기능을 하는 node들을 묶어 하나의 package를 만들고, 같은 source를 가지는 여러 package를 묶어 workspace(작업공간)를 만든다. 즉, 내가 작업하고자 하는 공간에서 여러 목적에 따라 pkg를 만들고 node를 작성한다고 보면 된다.
이렇게 말하면 어려워 보이지만, 쉽게 말해 내가 작업할 공간 = 하나의 directory가 workspace, 그 안에 pkg라는 여러 directory가 들어있고, pkg 안에 source code file인 node가 하나 이상 들어있다고 보면 된다.
ROS2에서는 여러개의 workspace에서 동시에 작업이 가능하다고 한다.아직 해본 적은 없다.
앞서 workspace가 하나의 디렉토리라고 이야기했다. workspace는 실제로 다음과 같은 구조를 가진다.
workspace_folder/
src/
cpp_package_1/
CMakeLists.txt
include/cpp_package_1/
package.xml
src/
py_package_1/
package.xml
resource/py_package_1
setup.cfg
setup.py
py_package_1/
...
cpp_package_n/
CMakeLists.txt
include/cpp_package_n/
package.xml
src/
workspace folder안에 src라는 폴더가 있고 이 안에 여러개의 패키지(python, c++ 둘다 가능)가 들어있는 형식이다. 보통 각 pkg의 src 폴더 안에 node(=source code file)이 하나 이상 들어있다.
작업 공간을 만들기 전, python package build를 위한 tool을 설치해준다.
sudo apt install python3-colcon-common-extensions
위의 코드로 colcon을 설치해주면 된다.
mkdir -p example_ws/src
cd example_ws/src
workspace 폴더와 그 안의 src 폴더를 만든다. workspace folder는 ws로 끝나는 게 국룰인듯 하다.
ros2 pkg create --build-type ament_python <package_name>
python pkg를 create 해준다. ament python을 통해 build하고, package name을 지정해주었다. 여기까진 필수 명령어이다. 옵션을 추가하면 아래처럼 쓸 수 있다.
ros2 pkg create --build-type ament_python <package_name> --node-name <node_name> --dependencies rclpy
node name을 써주면 예시 노드를 작성해줘서 편리하다. 예시 노드는 아래에서 살펴보겠다. 또한 depend하는 package가 있는경우 --dependencies를 통해 추가해주면 된다. 이는 나중에 추가할 수 있어서 굳이 지금 안해도 되긴 하는데(파일을 작성하면서 dependencies가 드러나는 경우가 많으니까), rclpy 정도는 해주는 편이다. rclpy에 대해서는 역시 node 작성하기에서 알아보겠다.
cd ..
colcon build --packages-select <package_name>
src 폴더에서 workspace 폴더로 이동하여 colcon build를 수행한다. 특정 package만 build하고 싶은 경우 --packages-select를 이용한다. node를 수정하면 매번 build해주어야 하므로 이 코드를 기억해두자.
source install/setup.bash
ros2 run <package_name> <node_name>
예시 노드를 만들었다면 아래와 같이 뜰 것이다.
Hi from <package_name>.
매번 sourcing 하는 게 귀찮으면 아래 command를 bashrc에 추가해주면 된다.
source ~/example_ws/install/setup.bash
이를 추가하면 새 터미널을 열 때부터 sourcing할 필요가 없어진다.
또한 ros2 run은 어느 디렉토리에서 입력하든 상관 없는 듯하다. 그래서 sourcing할 때 local_setup.bash를 입력해주면 그 워크스페이스의 패키지를 우선으로 sourcing해온다고 한다. 무슨 말인지 모르겠으면 복잡하니 넘어가자.
이제 pkg 폴더 내에 package.xml을 수정해주어야 한다. 앞서 예시처럼 pkg를 build하면 아래와 같은 package.xml 파일이 생긴다.
<?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>ex_pkg</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="example@gmail.com">example</maintainer>
<license>TODO: License declaration</license>
<depend>rclpy</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
위 package.xml에서 version이나 descriptyion, maintainer email, TODO 등을 수정해주면 된다. 또, pkg 안의 node를 작성하며 dependency하는 pkg가 생겼을 경우 rclpy 밑에 다음을 추가해주면 된다.
<depend>another_pkg</depend>
depend 태그에는 build depend, exec depend 등 여러가지 버전이 있는데 그냥 depend를 쓰면 빌드 시와 실행 시 의존성을 한 번에 정의할 수 있어 편리하다.
또 의존성 패키지가 많아지면 어떤 패키지를 써야하는지 일일이 기억하기는 어려운데, 이 때 아래 명령어를 입력하여 확인해볼 수 있다.
rosdep install -i --from-path src --rosdistro humble -y
node file은 example_ws/src/example_pkg/example_pkg 안에 추가해주면 된다. 작성 방법은 바로 뒤에서 설명한다.
새로운 node를 작성했다면 pkg 폴더에 있는 setup.py file을 다음과 같은 방식으로 수정해준다.(executable로 만들고 싶은 경우에만)
entry_points={
'console_scripts': [
'node_name = pkg_name.file_name:main'
],
node file을 수정하고 나서는 매번 colcon build를 수행해주어야 한다.
아까 create한 예시 node를 살펴보며 node를 작성해보자.
def main():
print('Hi from ex_pkg.')
if __name__ == '__main__':
main()
node는 이와 같이 아주 단순한 파이썬 파일로 되어있다. 실제로 우리는 ros2 run을 통해서 각 node의 main문을 실행하게 된다.(entry points 참고)
하지만 node는 단순히 hello world를 출력하는 것 이상의 기능을 해야한다. 간단한 code를 통해 pubilsher와 subscriber를 작성해보자.
ex_ws/src/ex_pkg/ex_pkg안에 아래와 같은 python file을 만들자.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
또한 다음과 같은 subscriber node도 만들자.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscriber = self.create_subscription(String, 'topic', self.listener_callback, 10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
두 코드를 조금 더 면밀히 살펴보자.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
ros에서 python을 쓰게 하는 rclpy와 ros에서 자체적으로 정의하는 표준 자료형인 String을 import 하고 있다. 이는 패키지 의존성에 들어가므로, 이렇게 import를 해준 뒤 package.xml을 아까 봤던 것 처럼 수정해야 한다.
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
publisher를 정의하는 class가 들어있는 부분이다. 기본적으로 Node간의 통신을 이용하기 위해서는 Node class를 상속해야 한다. 그러면 super의 생성자의 Node 이름을 넣어 initiate할 수 있다.
publisher와 subscriber는 다음과 같은 형태로 만든다. 보통 클래스의 인스턴스 변수로 저장하고, 필요할 때 호출하는 형식으로 이용한다.
self.publisher_ = self.create_publisher(String, 'topic', 10) #자료형, 토픽이름, queue size
self.subscriber = self.create_subscription(String, 'topic', self.listener_callback, 10) # 자료형, 토픽이름, 콜백함수, 큐 사이즈
creat_publisher와 create_subscription을 통해 만든다. 공통적으로는 자료형, 토픽 이름, queue size(또 추가적으로 QoS profile을 넣기도 하는데, 이는 나중에 살펴보도록 하겠다.)를 넣어준다.
pubilsher는 이렇게 선언한 뒤 publish(msg) 메소드를 이용하여 토픽을 발행한다. subscriber는 메소드에 콜백함수를 넘겨 토픽이 도착할 때마다 콜백함수가 호출되게 한다.
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
main문은 공통적이다. rclpy.init을 통해 ros2 시스템과 통신하기 위한 초기화 작업을 수행한다. Node를 상속하는 클래스를 선언해주고, rclpy.spin()에 이 클래스의 인스턴스를 넣어준다. 이 함수는 노드가 종료될 때까지 계속 실행되도록 만든다.(계속해서 publish하고 subscribe 하게 됨, 메세지를 받거나 주기적인 작업을 수행하는 노드를 지속적으로 실행시키는 데 사용.). 만약 main문을 종료하게 되면 node를 종료하고, shutdown을 통해 명시적으로 통신을 중단하게 된다.
ROS의 작업공간인 workspace와 그 안에서 특정 기능을 하는 package, 실행 가능한 단위인 node를 직접 만들어보았다. 그 과정에서 package의 dependency나 entry point를 정의하는 방법, package를 build하는 방법, node를 작성하는 방법 등을 알 수 있었다.
다음 포스팅을 통해서는 여러개의 node를 한 번에 실행시키는 방법을 알아보도록 하겠다.