2026년 3월 31일 화요일

Embedded Linux Programming with STM32MP257F-DK board(Episode VI)

거의 6년만에 STM32MP 보드를 다시 하나 입수했다(이번에는 STM32MP257F-DK 보드다). 이번 시간 부터는 앞으로 몇차례에 걸쳐서, STM32MP257F-DK 보드 기반 embedded linux와 관련한 다양한 주제를 다뤄 보고자 한다. 이번이 그 여섯번째 시간이다. 😎

 
The 6th episode

목차
10. Application 돌려 보기 - ROS 2, WireGuard Rust, Qt6
References

Keywords: ROS 2, nodes, topics, services, actionsparameters, launch files, ros2 cli tool, robotics


10. Application 돌려 보기 - ROS 2 [기초편]
ROS 2(Robot Operating System 2)산업용 로봇, 자율주행차, 드론, 서비스 로봇 등과 같은 분야에서 다양하게 사용되는 로봇 운영체제(실제로는 일반 OS 개념은 아니며, robot S/W framework or Stack으로 이해하면 됨)이다. 이번 장에서는 (ROS 2 용으로 설계된 것은 아니지만) STM32MP257F-DK 보드 상에서 ROS 2 기반의 robotic application(node)을 만들어 돌려 보고자 한다. 😎

[그림 10.0.1] ROS2 logo [출처 - 참고문헌 13]

초기 로봇 운영체제인 ROS 1은 단일 로봇 시스템에서의 학술적 연구와 신속한 프로토타이핑을 목적으로 설계되어 로보틱스 생태계에 혁명적인 기여를 하였다. 그러나 단일 중앙 마스터(ROS Master)에 의존하는 구조, 실시간(Real-time) 제어의 부재, 불안정한 무선 네트워크 환경에서의 통신 단절, 그리고 다중 로봇 시스템(Multi-Robot System) 구축의 한계 등 상용 제품 및 산업 현장에 적용하기에는 명확한 아키텍처적 결함이 존재했다. 이러한 근본적인 한계를 극복하기 위해 ROS 2는 통신 계층부터 클라이언트 라이브러리, 그리고 실행 스레딩 모델에 이르기까지 시스템의 전체 스택을 완전히 재설계하였다. 독자적인 통신 프로토콜을 폐기하고 산업 표준 미들웨어를 채택하였으며, 노드의 생명주기 제어 메커니즘을 도입하여 시스템의 결정론적(Deterministic) 동작을 보장하도록 진화하였다.

[그림 10.0.2] ROS 2 S/W Stack(1) [출처 - 참고문헌 17]
📌 실제로 ROS 2 framework은 RCL(ROS Client Layer) + RMW(ROS Middleware Layer)로 이루어져 있다.
📌 ROS 2의 node 간의 실제 통신(sending/receiving)은 DDS(Data Distribution Service)에서 진행된다. DDS는 외부 vendor(or community)에서 구현한 Fast DDS, Cyclone DDS, RTI Connext DDS 등을 이용한다.
📌 rcl 자체는 C로 구현되어 있으며, 다른 language들이 이를 wrapping하여 사용하는 것으로 이해하면 된다.

[그림 10.0.3] ROS 2 S/W Stack(2) [출처 - 참고문헌 19]

10.1 ROS 2 for Yocto Project
먼저, 이번 절에서는 지난 5, 6 장에서 살펴 보았던 Yocto project 환경에 ROS 2(Robot Operating System 2 - Humble)를 추가하고, 이를 사용하는 방법을 소개하도록 한다. 사실, ROS 2 내용 자체는 한권의 책으로 소개해도 모자랄 만큼, 방대한 내용으로 이루어져 있다. 따라서 이번 posting을 통해 모든 내용을 소개할 수는 없는 노릇이고, 우선은 기본 개념(node 생성/통신 및 관련 programming)을 중심으로 기초적인 내용을 소개해 보도록 하자. 😋

[그림 10.1.1] ROS2 Humble Hawksbill logo [출처 - 참고문헌 14]
📌 ROS 2 Humble이 old version이긴 하지만, 현재 사용 중인 PC 환경(Ubuntu 22.04 LTS)를 감안하여, 이 버젼을 사용해 보기로 한다.

$ cd <YOUR_PATH>/layers
git clone https://github.com/ros/meta-ros.git -b scarthgap
 -> meta-ros layer를 clone한다.
$ ls -la
drwxrwxr-x 13 chyi chyi 4096  3월  1 21:27 meta-openembedded
drwxrwxr-x 15 chyi chyi 4096  3월 20 11:21 meta-ros
drwxrwxr-x  7 chyi chyi 4096  3월  4 11:45 meta-st
drwxrwxr-x  9 chyi chyi 4096  3월  2 15:47 openembedded-core

DISTRO=openstlinux-weston MACHINE=stm32mp2 source layers/meta-st/scripts/envsetup.sh

$ bitbake-layers add-layer ../layers/meta-ros/meta-ros-common
NOTE: Starting bitbake server...
NOTE: Started PRServer with DBfile: /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/build-openstlinuxw
eston-stm32mp2/cache/prserv.sqlite3, Address: 127.0.0.1:37961, PID: 12817


$ bitbake-layers add-layer ../layers/meta-ros/meta-ros2
NOTE: Starting bitbake server...
NOTE: Started PRServer with DBfile: /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/build-openstlinuxw
eston-stm32mp2/cache/prserv.sqlite3, Address: 127.0.0.1:46373, PID: 12946

$ bitbake-layers add-layer ../layers/meta-ros/meta-ros2-humble
NOTE: Starting bitbake server...
NOTE: Started PRServer with DBfile: /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/build-openstlinuxweston-stm32mp2/cache/prserv.sqlite
3, Address: 127.0.0.1:44143, PID: 14560


$ bitbake-layers show-layers                 
NOTE: Starting bitbake server...
NOTE: Started PRServer with DBfile: /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/build-openstlinuxweston-stm32mp2/cache/prserv.sqlite
3, Address: 127.0.0.1:45141, PID: 16800
layer                 path                                                                    priority
========================================================================================================
meta-python           /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-python  5
openembedded-layer    /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-oe  5
gnome-layer           /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-gnome  5
multimedia-layer      /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-multimedia  5
networking-layer      /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-networking  5
webserver             /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-openembedded/meta-webserver  5
stm-st-stm32mp        /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-st/meta-st-stm32mp  6
st-openstlinux        /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-st/meta-st-openstlinux  5
core                  /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/openembedded-core/meta  5
workspacelayer        /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/build-openstlinuxweston-stm32mp2/workspace  99
meta-my-custo-layer   /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-st/meta-my-custo-layer  7
ros-common-layer      /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-ros/meta-ros-common  10
ros2-layer            /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-ros/meta-ros2  11
ros2-humble-layer     /mnt/hdd/workspace/ST/STM32MP257K_DK/Distribution-Package/layers/meta-ros/meta-ros2-humble  12


$ vi conf/local.conf
 -> 파일 맨 마지막에 아래 내용을 추가한다.

[그림 10.1.2] conf/local.conf 파일 수정

$ bitbake st-image-weston
 -> 헐, meta-ros를 build하는 것도 꽤나 오랜 시간을 잡아 먹는다.

정상 build가 진행되었으니, 1 장에서 소개한 내용처럼, STM32CubeProgrammer를 사용하여 SDcard Image를 다시 설치하도록 한다.

[그림 10.1.3] STM32CubeProgrammer로 sdcard image 설치

<Target board>

[그림 10.1.4] STM32MP257F-DK 보드 구동 모습

root@stm32mp2-e3-d2-e5:~# cd /opt/ros/humble
 -> ros2 humble이 설치된 위치를 찾아 보니, 아래와 같다.

[그림 10.1.5] ros2 humble이 설치된 위치

root@stm32mp2-e3-d2-e5:~# cd ~
root@stm32mp2-e3-d2-e5:~# bash
root@stm32mp2-e3-d2-e5:~# source /opt/ros/humble/setup.bash
root@stm32mp2-e3-d2-e5:~# ros2 node list
root@stm32mp2-e3-d2-e5:~# ros2 topic list                                                                        
/parameter_events
/rosout


root@stm32mp2-e3-d2-e5:~# ros2 service list
root@stm32mp2-e3-d2-e5:~# ros2 action list                                                                       

root@stm32mp2-e3-d2-e5:~# ps aux|grep ros2
[그림 10.1.6] ros2 node list 후 python3 process 확인 모습

OK, 일단 ros2가 제대로 설치되었다.

10.2 ROS 2 Node 개요
이번 절에서는 ROS 2의 주요 구성 요소를 확인한 후, (가장 먼저) ROS 2 node에 관하여 살펴보기로 한다.

<ROS 2의 주요 구성 요소>
  • 노드(Node): ROS 2 framework의 기본 실행 단위로, 특정 기능을 수행하며 다른 노드와 통신하여 데이터를 주고받는다.
  • 토픽(Topic): 노드 간의 데이터 송수신을 위한 통신 채널로, Publisher가 데이터를 게시하고 Subscriber가 데이터를 수신한다.
  • 서비스(Service): Request/Response 방식의 통신을 제공하여, 클라이언트 노드가 서버 노드에 요청을 보내고 응답을 받는다.
  • 액션(Action): 서버에서 처리하는 작업이 오래 걸리는 경우를 위한 통신 방식으로, 진행 상황과 결과(Result)를 피드백(Feedback)할 수 있다.
  • 파라미터(Parameter): 노드의 설정 값을 저장하고 변경할 수 있는 기능으로, 로봇의 동작을 동적으로 조정할 수 있다.

ROS 2를 설치하면 몇가지 테스트 application(node)을 사용할 수가 있다. 이 중에서 C++로 만들어진 talker와 listener app을 실행해 보기로 하자.

talker -> "Hello World" -> listener

root@stm32mp2-e3-d2-e5:~# 
ros2 run demo_nodes_cpp talker
 -> 먼저 talker를 실행한다.

[그림 10.2.1] ros2 run demo_nodes_cpp talker
📌 ros2 run <package> <executable> 형태로 ros2 node를 구동한다.

root@stm32mp2-e3-d2-e5:~# ros2 run demo_nodes_cpp listener
 -> 다음으로 listener를 띄워 본다.

[그림 10.2.2] ros2 run demo_nodes_cpp listener

시험결과에서 알 수 있듯이, talker가 "Hello World" string을 내보내면, listener가 이를 받아 console에 출력하는 간단한 program이다.
참고로, talker & listener는 /opt/ros/humble/lib/demo_nodes_cpp 디렉토리에서 확인 가능하다.

[그림 10.2.3] demo_nodes_cpp 패키지 안의 내용
📌 ros에서 말하는 node는 package안에 포함되어 있는 실행 파일이다. 단, node이름과 실행 파일명이 항상 동일한 것은 아니다.

이 상태에서 동작 중인 process 상태를 확인해 보면 다음과 같다.

$ ps aux | grep ros2
 11357 ttySTM0  S+     0:01 python3 /opt/ros/humble/bin/ros2 run demo_nodes_cpp talker
 11361 ttySTM0  Sl+    0:00 /opt/ros/humble/lib/demo_nodes_cpp/talker
 11386 pts/0    S+     0:01 python3 /opt/ros/humble/bin/ros2 run demo_nodes_cpp listener
 11387 pts/0    Sl+    0:00 /opt/ros/humble/lib/demo_nodes_cpp/listener
📌 ros2는 python3 code에 불과하다. ros2 run에서 talker or listener (별도의) application을 실행하고 있다.

ros2 run <package> <executable> 명령이 대략 어떤 식으로 동작하는지 감이 오는가 ? 😋

한편, 위의 내용은 아래와 같이 node list 명령으로도 간단히 확인 가능하다.

root@stm32mp2-e3-d2-e5:~# ros2 node list
/listener
/talker

-----------------------------------------------------
ROS 2 node는 아래와 같은 3가지 방식(Topic, Service, Action)을 이용하여 상호통신을 하게 된다.

1) 토픽(Topic) 기반의 Publisher(발행자) & Subscriber(구독자)
2개 이상의 node가 topic을 매개체로 하여 상호간 통신하는 방식을 말한다. 즉, 특정 node가 topic을 이용하여 data를 publish(발행)하면, 다른 node들은 해당 topic에 대해 subscribe(구독)하는 형태로 동작한다(단방향 통신). 한편, topic은 name과 interface(= or data type)를 이용하여 정의된다.

[그림 10.2.4] ROS 2 topic 기반의 publisher & subscriber 개념도
📌 경우에 따라서 publisher와 subscriber를 모두 가진 node를 만들 수도 있다.
📌 topic은 name, interface 외에도 QoS 정책(policies)으로 구성되어 있다.

2) 서비스(Service) 기반의 Client & Server
Node간의 data 교환을 위해, publisher/subscriber 말고도 client/server 형태로 통신(양방향 통신, 요청에 대한 즉각적인 응답)하는 방식을 생각할 수 있다. 이 경우 client와 server간에 공유하는 정보를 service라고 부른다. service 역시 topic과 마찬가지로, name과 interface로 정의하게 되는데, (topic과) 차이가 있다면, service의 Interface는 request와 response로 구성되어 있다는 점이다.

[그림 10.2.5] ROS 2 service 기반의 client & server 개념도
📌 보통, 동일한 1 개의 server에 대해 여러개의 client가 연결되는 구조인데, 이 경우에는 반드시 모두 동일한 service(name + interface)를 사용해야 한다.

3) 액션(Action) 기반의 Client & Server
Action은 service와 마찬가지로, node간의 data 교환을 위해 (기본적으로는) client/server 형태로 통신하는 방식이다. 다만, service 방식과 차이가 있다면, server에서의 응답 처리 시간(execution time)이 오래 소요되는 경우를 감안하여, interface가 request/response 형태로 단순하게 구성된게 아니라, goal(request/response), result(request/response), feedback(publish) 이라는 3가지 요소로 구성되어 있다는 점이다.

[그림 10.2.6] ROS 2 action 기반의 client & server 개념도

 Goal: client가 action server에게 전달하는 요청(최종 목표)으로, server는 goal에 대한 응답(response)를 곧 바로 내 보내주게 된다(service 개념).
• Result: action server가 최종적으로 도달한 결과 값(return 값)으로, Goal에 도달한 값일 수도 있고, 중도에 문제(장애 혹은 client로 부터의 cancel 요청)가 발생한 시점에서 확인한 결과 값일 수도 있다. 내부적으로는 client가 Result에 대한 request를 날리고, server가 Result에 대한 response를 내보내는 형태로 구현된다(service 개념).
• Feedback: 최종 Goal에 도달하기 전에 주기적으로 현재 상태를 알려 주는 행위로 topic 기반의 publish 개념을 사용하는 것으로 이해하면 된다(topic 개념). Feedback이 필요한 이유는 server에 요청한 최종 Goal에 도달하기까지 많은 시간이 소요될 것이기 때문에, 중간 상태를 주기적으로 client에게 알려주기 위해서이다.


[그림 10.2.7] ROS 2 Node간 통신 방식 비교

10.3 ROS 2 Node 만들기 기초
지금부터는 ROS 2 node를 만드는 과정을 소개해 보고자 한다. ROS 2 node는 (기본적으로) Python과 C++로 만들 수가 있다. 여기서에서는 C++ 방법을 기준으로 설명하기로 한다.
📌 이 밖에도 C, Java/Android, Go, Rust, Node.js, C#, Dart 등의 language를 사용하여 node를 구현할 수가 있다.

ROS 2 관련하여 기본적인 내용을 확인하기 위하여, 지금부터는 편의상 (10.1, 2절에서 언급한 Yocto project & ROS 2 Humble 대신) Ubuntu 24.04 LTS와 ROS 2 Jazzy 환경을 기준으로 테스트를 진행하고자 한다. 😂

[그림 10.3.1] ROS 2 Jazzy Jalisco on Ubuntu 24.04

ROS2 Jazzy version 설치와 관련해서는 아래 link를 참조하기 바란다(설치 과정은 크게 어렵지 않으니 따로 정리하지는 않는다 😋).

또한, 이번 posting에서 소개하는 예제는 아래 Github code를 참조하였음을 밝힌다.
📌 ROS 2 from Scratch book은 내용이 알차고, 매우 쉽게 기술되어 있으므로, ROS 2 초심자들에게 강추한다. 💯

자, 그럼 이제 부터 C++ 기반의 ROS 2 node를 생성하고 build하는 방법을 살펴 보도록 하자.

<Ubuntu 24.04 LTS>
$ source /opt/ros/jazzy/setup.bash
📌 ~/.bashrc에 넣어두면 편리하다.

먼저 아래와 같이 workspace(디렉토리에 불과함)를 하나 만들자.
mkdir ~/ros2_ws
cd ros2_ws/
$ mkdir src

비워있는 workspace이지만, (ROS 2 build root인)colcon을 이용하여 build를 시도해 본다.

$ cd ~/ros2_ws/
$ colcon build
Summary: 0 packages finished [0.73s]

colcon build 수행 결과, 아래와 같은 디렉토리가 자동 생성되는 것을 알 수 있다.
$ ls
build  install  log  src

다음으로 workspace 내에 package를 하나 만들어 보자.
📌 당연한 거지만, workspace 내에 여러개의 package를 만들 수 있다. 또한 1 개의 package 안에는 여러 개의 node가 포함될 수 있다.

$ cd ~/ros2_ws/src/
$ ros2 pkg create my_cpp_pkg --build-type ament_cmake --dependencies rclcpp

~ /ros2_ws/src/my_cpp_pkg/
├── CMakeLists.txt
├── include
│   └── my_cpp_pkg
├── package.xml
└── src

역시 cpp file이 전혀 포함되지 않은 빈 package이지만, colcon으로 build를 시도해 보도록 하자.
$ cd ~/ros2_ws/
$ colcon build --packages-select my_py_pkg
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [3.46s]
Summary: 2 packages finished [3.72s]

이제 부터, cpp node를 본격적으로 만들어 볼 차례이다.
$ cd ~/ros2_ws/src/my_cpp_pkg/src/
$ vi my_first_node.cpp
-> timer를 만들어 1초 간격으로 Hello <counter> string을 출력하는 간단한 코드이다.

[그림 10.3.2] cpp node example - my_first_node.cpp
📌 VSCode 등을 사용하여 편집하도록 하자.

main 함수는 아래와 같은 형태로 구성되어 있다.
-----------------------------------------------------------
int main(int argc, char **argv) {
    rclcpp::init(argc, argv); //rcl(ros client layer)를 초기화 한다.
    auto node = std::make_shared<NumberPublisherNode>(); //NumberPublisherNode라는 이름의 node를 생성한다(shared_ptr smart pointer 사용).
    rclcpp::spin(node); //loop을 돌면서 대기한다. 모든 node의 동작은 이 상태에서 진행된다. (예를 들어, subscriber라고 하면) 즉, 메시지 수신하고, 앞 단계에서 등록된 다양한 callback이 호출하여 이를 처리한다.
    rclcpp::shutdown(); //종료시 호출되는 함수이다.
    return 0;
}
-----------------------------------------------------------

colcon은 기본적으로 cmake를 기반으로 build를 진행한다. 따라서 앞서 만든 my_first_node.cpp 파일이 compile될 수 있도록 CMakeLists.txt 파일을 아래와 같이 수정한다.

<파일 중간에 아래 내용 추가>
add_executable(test_node src/my_first_node.cpp)    //cpp file이 여러개이면 계속 추가해 준다(각각의 cpp 파일은 space로 구분).
ament_target_dependencies(test_node rclcpp)   //test_node가 사용하는 library를 기술해준다.
install(TARGETS
        test_node              //test_node를 설치할 위치를 잡아준다. 역시 여러개의 node를 테스트하려면, 각 line에 하나씩의 node 명을 입력해 준다.
         DESTINATION lib/${PROJECT_NAME}/
)

[그림 10.3.3] CMakeLists.txt 파일 example

colcon build를 다시 한다.

$ cd ~/ros2_ws/
$ colcon build --packages-select my_cpp_pkg
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [0.11s]                  

Summary: 1 package finished [0.25s]

이후, 아래 위치에 binary file이 만들어 진다.

$ cd install/my_cpp_pkg/lib/my_cpp_pkg
$ ls -l 
-rwxr-xr-x 1 chyi chyi 206744  3월 23 12:44 test_node

OK, node가 준비되었으니 돌려 보도록 하자.

$ source ~/ros2_ws/install/setup.bash
$ ros2 run my_cpp_pkg test_node
[INFO] [1774242326.641557867] [my_node_name]: Hello 0
[INFO] [1774242327.641536203] [my_node_name]: Hello 1
[INFO] [1774242328.641652731] [my_node_name]: Hello 2
[INFO] [1774242329.641642273] [my_node_name]: Hello 3
[INFO] [1774242330.641552692] [my_node_name]: Hello 4
[INFO] [1774242331.641552492] [my_node_name]: Hello 5
[INFO] [1774242332.641700337] [my_node_name]: Hello 6

OK, 드디어 C++ 기반의 node 하나가 제대로 만들어 졌다. 😎

10.4 Topic 기반의 Publisher & Subscriber Node 구현하기
이번 절에서는 특정 topic을 사용하는 1 개의 publisher(sender)와 1 개의 subscriber(receiver) node를 만들어 상호 통신하는 내용을 소개해 보고자 한다.

[그림 10.4.1] topic을 사용하는 publisher & subscriber 예제 개요도

먼저 topic = name + interface(= message) 형태로 구성되어 있다. name은 글자 그대로 topic을 구분하는 identifier(예: "number")가 되겠고, interface는 전달하려는 message(예: example_interfaces/msg/Int64)으로 이해하면 된다.

a) C++ 기반 Publisher node 만들기
그럼, 이제부터 본격적으로 publisher node를 위한 C++ code를 작성해 보도록 하자.

$ cd ~/ros2_ws/src/my_cpp_pkg/src/
vi number_publisher.cpp
 -> timer를 사용하여 1초 간격으로 Integer(Int64) 값 2를 publish하는 코드이다.

[그림 10.4.2] publisher example - number_publisher.cpp

위의 코드 내용 중, (가장 핵심적인 내용이라고 할 수 있는) class NumberPublisherNode 생성자에서 하는 일을 요약해 보면 다음과 같다.
----------------------------------------------------------------------
    m_number_publisher = this->create_publisher<example_interfaces::msg::Int64>("number", 10);   //topic name => "number", interface => example_interfaces::msg::Int64
                            //publisher를 만든다(shared ptr)
    m_number_timer = this->create_wall_timer(   //timer method, 1초 간격으로 publishNumber method 호출
                                std::chrono::seconds(1),
                                std::bind(&NumberPublisherNode::publishNumber, this));  //std::bind는 function pointer 설정
----------------------------------------------------------------------

다음으로, package.xml과 CMakeLists.txt 파일을 차례로 수정해 준다.

$ cd  ..
$ vi package.xml
...
  <depend>rclcpp</depend>
  <depend>example_interfaces</depend>    //위의 cpp code에서 examples_interfaces package를 사용하고 있으므로, 이 라인을 추가한다.
...

vi CMakeLists.txt   //아래 주황색 라인을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

install(TARGETS
    test_node
    number_publisher
    DESTINATION lib/${PROJECT_NAME}/
)
...

$ cd ~/ros2_ws/
$ colcon build --packages-select my_cpp_pkg
 -> colcon build를 수행한다.

$ source ~/.bashrc
$ ros2 run my_cpp_pkg number_publisher
 -> 실행 파일이 만들어 졌으니, 이를 실행하도록 한다.
[INFO] [1774328636.301017068] [number_publisher]: Number publisher has been started.

자, publisher node가 준비되었으니, 이제부터는 subscriber node를 만들 차례이다.

b) C++ 기반 Subscriber node 만들기
$ cd ~/ros2_ws/src/my_cpp_pkg/src/
vi number_subscriber.cpp
 -> 동일한 topic(name: "number", interface: Int64)을 매개로 하여 값을 읽어들인 후, 이를 이전 값에 더한 후 출력한다.

[그림 10.4.3] subscriber example - number_counter.cpp

class NumberSubscriberNode 생성자와 subscriber callback 함수에서 하는 일을 요약해 보면 다음과 같다.
-------------------------------------------------------------------------------------------------------------
        m_number_subscriber = this->create_subscription<example_interfaces::msg::Int64>(
                "number", 10, std::bind(&NumberCounterNode::callbackNumber, this, _1));   //subscriber 객체를 생성하고, topic name으로 "number"를, queue size로 10을, 마지막으로  callbackNumber 함수를 파라미터러 넘긴다.
        RCLCPP_INFO(this->get_logger(), "Number Counter has been started.");

-------------------------------------------------------------------------------------------------------------
callback 함수는 subscriber가 수신한 message를 전달 받아, m_counter변수에 더한 후 로그로 출력한다.

    void callbackNumber(const example_interfaces::msg::Int64::SharedPtr msg) {
        m_counter += msg->data;   //수신한 값을 이전 값에 더한다.
        RCLCPP_INFO(this->get_logger(), "Counter: %d", m_counter);
    }
-------------------------------------------------------------------------------------------------------------

$ cd ..
vi CMakeLists.txt   //아래 주황색 라인을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

add_executable(number_counter src/number_counter.cpp)
ament_target_dependencies(number_counter rclcpp example_interfaces)

install(TARGETS
    test_node
    number_publisher
    number_counter
    DESTINATION lib/${PROJECT_NAME}/
)
..

$ cd ~/ros2_ws/
colcon build --packages-select my_cpp_pkg
 -> colcon build를 수행한다.

$ source ~/.bashrc
ros2 run my_cpp_pkg number_counter
 -> 실행 파일이 만들어 졌으니, 이를 실행하도록 한다. OK, 원하는 대로 publisher가 보낸 data를 계속 더하여 출력하고 있다. 😎
[INFO] [1774332719.442314849] [number_counter]: Number Counter has been started.
[INFO] [1774332720.302325123] [number_counter]: Counter: 2
[INFO] [1774332721.302149987] [number_counter]: Counter: 4
[INFO] [1774332722.301796054] [number_counter]: Counter: 6
[INFO] [1774332723.302275903] [number_counter]: Counter: 8
[INFO] [1774332724.302323355] [number_counter]: Counter: 10
-------------------------------------------

참고로, ROS 2에는 현재 동작 중인 node 들의 연결 관계를 graph 형태로 확인하는 기능이 있다.

$ rqt_graph
 -> 아래 그림은 방금 테스트한 number_pulisher와 number_counter (subscriber) node가 /number topic으로 연결되어 있음을 보여준다.

[그림 10.4.4] rqt_graph로 publisher -> topic -> subsriber 간의 관계 확인

이 밖에도 유용한 명령이 많이 있다.
먼저, 아래 명령은 topic 정보를 확인하기 위해 사용된다.
$ ros2 topic info /number
Type: example_interfaces/msg/Int64
Publisher count: 1
Subscription count: 1

한편, 아래 명령은 특정 interface에 대한 정보를 확인하기 위해 사용한다.
$ ros2 interface show example_interfaces/msg/Int64
# some comments
int64 data

마지막으로, 아래 명령을 사용하면 지정된 topic(예: /number)에 대해 subscriber를 흉내낼 수가 있다.
$ ros2 topic echo /number
data: 2
---
data: 2
---

10.5 Service 기반의 Client & Server Node 구현하기
이번 절에서는 Service 개념을 사용하는 Client & Server node를 구현하여 돌려 보도록 하자. Service도 Topic과 마찬가지로 name과 interface로 구성되어 있다. 즉,
service = name + interface (= 전달하려는 message)

다만, Service의 interface는 client -> server로 보낼때 사용하는 Request 정보와 server -> client로 되돌려 보내는 Response 정보로 다시 세분화되게 된다. 즉,
service = name + interface (= Request message + Response message)

이번 절에서 구현하고자하는 service interface와 client & server node의 관계를 그림으로 표현해 보면 다음과 같다.

[그림 10.5.1] service를 사용하는 client & server 예제 개요도
📌 이번 절에서 구현하는 내용은 Node#2(reset_counter_client)와 Node#3(number_counter)이다. Node#1은 이전 절에서 만든 것을 그대로 사용한다.
📌 Node#2 service client(= reset_counter_client)는 server로 부터의 응답 처리를 위해 비동기 programming(async & future 기반) 기법을 이용한다.

a) 사용자 정의 Service interface 만들기
ros2 pkg create my_robot_interfaces
  -> 이 절에서 사용할 사용자 정의 Service interface를 위해 1 개의 package를 만들어 보자.

$ cd my_robot_interfaces/
rm -rf src/ include/
$ mkdir srv

vi package.xml    //아래 주황색 Line을 추가한다.
...
  <buildtool_depend>ament_cmake</buildtool_depend>
  <build_depend>rosidl_default_generators</build_depend>
  <exec_depend>rosidl_default_runtime</exec_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>
...

vi CMakeLists.txt     //아래 주황색 Line을 추가한다.
...
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
   # we will add the name of our custom interfaces here
)
ament_export_dependencies(rosidl_default_runtime)
...

$ cd srv
vi ResetCounter.srv
  -> 아래의 내용으로 구성된 service file을 하나 만든다.  --- 라인을 기준으로 Request, Response message field가 구분된다.
int64 reset_value
---
bool success
string message

cd ..
vi CMakeLists.txt   //아래 주황색 Line을 추가한다.
...
 find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  # we will add the name of our custom interfaces here
  "srv/ResetCounter.srv"
)
ament_export_dependencies(rosidl_default_runtime)
..

$ cd <YOUR_DIR>/ros2_ws/
colcon build --packages-select my_robot_interfaces
  -> 새로 추가한 package를 build한다.
Starting >>> my_robot_interfaces
Finished <<< my_robot_interfaces [4.37s]                    

Summary: 1 package finished [4.51s]

ros2 interface show my_robot_interfaces/srv/ResetCounter
 -> 새로 생성한 service interface가 제대로 build되었는지 확인해 본다
int64 reset_value
---
bool success
string message

자, 이제 부터는 방금 만든 service interface를 사용하는 client, server node를 구현해 보도록 하자.

b) C++ 기반의 Service Server 만들기
$ cd src/my_cpp_pkg/src
$ vi number_counter.cpp
 -> 10.4절에서 만든 subscriber code 위에 service client가 전달한 요청(reset_value)을 받아 이를 처리(현재 count 값은 reset_value로 reset)하는 코드이다.
 -> 따라서, 10.4절에 만든 publisher를 구동한 상태에서 동작을 확인해야 한다.

[그림 10.5.2] service server example - number_counter.cpp
(기존 subscriber code에 service server code 추가)

$ cd ..
$ vi package.xml   //아래 주황색 Line을 추가한다.
...
  <depend>rclcpp</depend>
  <depend>example_interfaces</depend>
  <depend>my_robot_interfaces</depend>
...

$ vi CMakeLists.txt   //아래 주황색 Line을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)
find_package(my_robot_interfaces REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

add_executable(number_counter src/number_counter.cpp)
ament_target_dependencies(number_counter rclcpp example_interfaces my_robot_interfaces)

install(TARGETS
    test_node
    number_publisher
    number_counter
    DESTINATION lib/${PROJECT_NAME}/
)
...

$ colcon build --packages-select my_cpp_pkg
  -> service server를 build한다.
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [0.13s]                  

Summary: 1 package finished [0.28s]

자, 이번에는 Service Client를 만들 차례다.

c) C++ 기반의 Service Client 만들기
$ cd src/my_cpp_pkg/src
$ vi reset_counter_client.cpp
 -> reset_value를 server에게 요청하고, 그 결과(success and message)를 돌려 받는다.


[그림 10.5.3] service client example - reset_counter_client.cpp

위의 내용 중, async programming과 관련한 부분을 발췌해 보면 다음과 같다.
-----------------------------------------------
   void callResetCounter(int value) {   //main() 에서 호출하는 method이다.
        ...
        m_client->async_send_request//client -> server로의 request(요청) message를 보낸다. async 형태로 되어 있어, (blocking되지 않고) 호출 즉시 return된다.
            request, std::bind(&ResetCounterClientNode::callbackResetCounterResponse, this, _1));  //client -> server로의 request(요청)에 대한 response(응답) message를 수신할 method를 callback 형태로 등록한다.
    }

    void callbackResetCounterResponse(rclcpp::Client<ResetCounter>::SharedFuture future) {  //server로 부터 응답이 도달할 경우, 호출된다.
        auto response = future.get() //future로 부터 response message를 얻어 온다.
        RCLCPP_INFO(this->get_logger(), "Success flag: %d, Message: %s", (int)response->success, response->message.c_str());
    }
-----------------------------------------------

$ cd ..
$ vi CMakeLists.txt   //아래 주황색 Line을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)
find_package(my_robot_interfaces REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

add_executable(number_counter src/number_counter.cpp)
ament_target_dependencies(number_counter rclcpp example_interfaces my_robot_interfaces)

add_executable(reset_counter_client src/reset_counter_client.cpp)
ament_target_dependencies(reset_counter_client rclcpp example_interfaces my_robot_interfaces)

install(TARGETS
    test_node
    number_publisher
    number_counter
    reset_counter_client
    DESTINATION lib/${PROJECT_NAME}/
)
...

$ colcon build --packages-select my_cpp_pkg
  -> service client를 build한다.
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [4.33s]                     

Summary: 1 package finished [4.47s]

--------------------------------------
2개의 서로 다른 terminal을 띄운 상태에서 앞서 만든 service server와 client를 구동시켜 보자. 단, 앞서도 언급한 바와 같이 service server는 10.4절에서 구현한 subscriber를 기반으로 하고 있으므로, 원하는 테스트를 위해서는 terminal을 한개 더 띄워 10.4절에서 만든 publisher를  함께 구동시켜 주어야 한다.

<Terminal#1>
$ source ~/.bashrc
$ ros2 run my_cpp_pkg number_counter     //service 방식의 server + topic 방식의 subscriber로 구성
 -> service server를 구동시킨다. 처음 구동 시에는 아무런 message도 출력되지 않는다.
 -> 아래 message는 <Terminal#2>에서 topic 기반의 publisher를 구동할 경우에 출력된다.
[INFO] [1774592677.531091509] [number_counter]: Number Counter has been started.
[INFO] [1774592762.128759082] [number_counter]: Counter: 2
[INFO] [1774592763.129129897] [number_counter]: Counter: 4
[INFO] [1774592764.129125306] [number_counter]: Counter: 6
[INFO] [1774592765.128698985] [number_counter]: Counter: 8
[INFO] [1774592766.129124574] [number_counter]: Counter: 10
[INFO] [1774592767.129235609] [number_counter]: Counter: 12
[INFO] [1774592768.129250167] [number_counter]: Counter: 14
[INFO] [1774592769.129222656] [number_counter]: Counter: 16
[INFO] [1774592770.129297244] [number_counter]: Counter: 18
[INFO] [1774592771.128706138] [number_counter]: Counter: 20
[INFO] [1774592772.128819342] [number_counter]: Counter: 22
[INFO] [1774592773.129214329] [number_counter]: Counter: 24
[INFO] [1774592774.129382321] [number_counter]: Counter: 26
[INFO] [1774592775.129144073] [number_counter]: Counter: 28
[INFO] [1774592775.163618650] [number_counter]: Reset counter to 20
  -> <Terminal#3>에서 service client를 구동할 경우에 출력된다.
[INFO] [1774592776.129176837] [number_counter]: Counter: 22
[INFO] [1774592777.128871342] [number_counter]: Counter: 24
[INFO] [1774592778.129141325] [number_counter]: Counter: 26
[INFO] [1774592779.129001660] [number_counter]: Counter: 28
[INFO] [1774592780.128905536] [number_counter]: Counter: 30
[INFO] [1774592781.129324478] [number_counter]: Counter: 32
[INFO] [1774592781.757310118] [number_counter]: Reset counter to 20
  -> <Terminal#3>에서 service client를 구동할 경우에 출력된다.
[INFO] [1774592782.128789490] [number_counter]: Counter: 22
[INFO] [1774592783.128967001] [number_counter]: Counter: 24
[INFO] [1774592784.128693339] [number_counter]: Counter: 26
[INFO] [1774592785.129419766] [number_counter]: Counter: 28
[INFO] [1774592786.129260989] [number_counter]: Counter: 30
[INFO] [1774592787.129142294] [number_counter]: Counter: 32

<Terminal#2>
$ source ~/.bashrc
$ ros2 run my_cpp_pkg number_publisher   //topic 방식의 publisher
 -> 10.4절에서 만든 topic 기반의 publisher를 구동시킨다.
[INFO] [1774592761.127894624] [number_publisher]: Number publisher has been started

<Terminal#3>
$ source ~/.bashrc
ros2 run my_cpp_pkg reset_counter_client   //service 방식의 client
[INFO] [1774592694.887649270] [reset_counter_client]: Success flag: 0, Message: Reset value must be lower than current counter value

^C[INFO] [1774592767.837497854] [rclcpp]: signal_handler(SIGINT/SIGTERM)

$ ros2 run my_cpp_pkg reset_counter_client
[INFO] [1774592769.046707460] [reset_counter_client]: Success flag: 0, Message: Reset value must be lower than current counter value
^C[INFO] [1774592774.012137580] [rclcpp]: signal_handler(SIGINT/SIGTERM)

$ ros2 run my_cpp_pkg reset_counter_client
[INFO] [1774592775.164367880] [reset_counter_client]: Success flag: 1, Message: Success
^C[INFO] [1774592780.543210022] [rclcpp]: signal_handler(SIGINT/SIGTERM)

-------------------------------------
OK, 지금까지 사용자 정의 service interface를 기반으로 하는 client, server node가 정상 동작하는 것을 확인해 보았다.

10.6 Action 기반의 Client & Server Node 구현하기
이번 절에서는 Action 개념을 사용하는 Client & Server node를 구현해 보도록 하자. Action은 Client로 부터 들어온 요청을 Server에서 처리함에 있어, 처리 시간이 오래 걸리는 경우(long-running tasks)에 사용하는 통신 방식으로, service(client <-> server 간의 request/reponse 처리용)와 topic(server -> client로 feedback, status 등을 전달하는 용도로 활용)을 혼합한 방식이라고 말할 수 있다.

먼저, 이번 절에서 구현하고자하는 action interface 및 client & server node의 관계를 그림으로 표현해 보면 다음과 같다.

[그림 10.6.1] action을 사용하는 client & server 예제 개요도

<시험 내용>
1) Client는 Server에게 target_number(정수 값)를 전달하고, Server는 0 부터 1씩 증가시켜 target_number에 도달하면 도달한 사실을 [Result : reached_number]에 실어서 client에 알린다
2) 이때, 장시간 executime time 소비 routine을 흉내내기 위해 delay 값을 전달하게 되는데, 이를 수신한 Server는 값이 1씩 증가시킬 때마다 delay 시간 만큼 지연 처리(sleep)를 하도록 한다.
3) 한편, Server는 0 부터 1씩 값을 증가시킬 때마다 이의 사실을 [Feedback: current_number]에 실어서 client에게로 보낸다.
4) Client는 (특정한 상황에서) Canel 명령을 Server로 전달하게 되는데, Server는 이 요청을 받는 경우, 하던 일을 중단하고 현재까지 진행된 상태를 [Result : reached_number]에 실어서 client에 알린다
-----------------------------------------------------------

자, 이제부터 action 기반의 client & server node에서 사용할 사용자 정의 action interface를 먼저 정의해 보도록 하자.

a) 사용자 정의 action  interface 만들기
$ cd ros2_ws/src/my_robot_interfaces/
$ mkdir action
$ cd action

vi CountUntil.action
 -> Service와 유사하게 ---로 Goal, Result, Feedback field를 구분해서 설정해 주어야 한다.
# Goal
int64 target_number
float64 delay
---
# Result
int64 reached_number
---
# Feedback
int64 current_number
📌 [주의] 반드시 goal, result, feedback 순으로 action field를 구성해 주어야 한다.

$ cd ..
$ vi CMakeLists.txt    //아래 주황색 Line을 추가한다.
...
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  # we will add the name of our custom interfaces here
  "msg/HardwareStatus.msg"
  "srv/ResetCounter.srv"
  "action/CountUntil.action"
)
ament_export_dependencies(rosidl_default_runtime)
...

$ cd ../..
$ colcon build --packages-select my_robot_interfaces
  -> 사용자 정의 action interface가 포함된 package를 build한다.
[0.231s] WARNING:colcon.colcon_core.package_selection:Some selected packages are already built in one or more underlay workspaces:
'my_robot_interfaces' is in: /mnt/hdd/workspace/ROS2/ros2_ws/install/my_robot_interfaces
If a package in a merged underlay workspace is overridden and it installs headers, then all packages in the overlay must sort their include directories by workspace order. Failure to do so may result in build failures or undefined behavior at run time.
If the overridden package is used by another package in any underlay, then the overriding package in the overlay must be API and ABI compatible or undefined behavior at run time may occur.

If you understand the risks and want to override a package anyways, add the following to the command line:
--allow-overriding my_robot_interfaces

This may be promoted to an error in a future release of colcon-override-check.
Starting >>> my_robot_interfaces
Finished <<< my_robot_interfaces [4.08s]                    

Summary: 1 package finished [4.22s]

위에서 발생한 warning을 없애고자 한다면, 아래와 같은 option을 주어 처리해 준다.

$ colcon build --packages-select my_robot_interfaces --allow-overriding my_robot_interfaces
Starting >>> my_robot_interfaces
Finished <<< my_robot_interfaces [6.91s]                    

Summary: 1 package finished [7.06s]

$ source ~/.bashrc
$ ros2 interface show my_robot_interfaces/action/CountUntil
 -> 사용자 정의 action interface가 제대로 만들어 졌는지 확인해 본다.
int64 target_number
float64 delay
---
int64 reached_number
---
int64 current_number

이제 부터, action 기반의 Client & Server node를 구현해 보도록 하자.

b) C++ 기반 Action Server 만들기
$ cd src/my_cpp_pkg/src
$ vi count_until_server.cpp


[그림 10.6.2] action server example - count_until_server.cpp

$ cd ..
$ vi package.xml   //아래 주황색 Line을 추가한다.
...
  <depend>rclcpp</depend>
  <depend>example_interfaces</depend>
  <depend>my_robot_interfaces</depend>
  <depend>rclcpp_action</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
..

$ vi CMakeLists.txt   //아래 주황색 Line을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)
find_package(my_robot_interfaces REQUIRED)
find_package(rclcpp_action REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

add_executable(number_counter src/number_counter.cpp)
ament_target_dependencies(number_counter rclcpp example_interfaces my_robot_interfaces)

add_executable(count_until_server src/count_until_server.cpp)
ament_target_dependencies(count_until_server rclcpp rclcpp_action my_robot_interfaces)

install(TARGETS
    test_node
    number_publisher
    number_counter
    count_until_server
    DESTINATION lib/${PROJECT_NAME}/
)
...

$ colcon build --packages-select my_cpp_pkg
 -> action server를 build한다.
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [4.51s]                     
                     
Summary: 1 package finished [4.65s]

c) C++ 기반 Action Client 만들기
$ cd src/my_cpp_pkg/src
$ vi count_until_client.cpp

[그림 10.6.3] action client example - count_until_client.cpp

$ cd ..
vi package.xml   //아래 주황색 Line을 추가한다.
...
  <depend>rclcpp</depend>
  <depend>example_interfaces</depend>
  <depend>my_robot_interfaces</depend>
  <depend>rclcpp_action</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
..

vi CMakeLists.txt   //아래 주황색 Line을 추가한다.
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)
find_package(my_robot_interfaces REQUIRED)
find_package(rclcpp_action REQUIRED)

add_executable(test_node src/my_first_node.cpp)
ament_target_dependencies(test_node rclcpp)

add_executable(number_publisher src/number_publisher.cpp)
ament_target_dependencies(number_publisher rclcpp example_interfaces)

add_executable(number_counter src/number_counter.cpp)
ament_target_dependencies(number_counter rclcpp example_interfaces my_robot_interfaces)

add_executable(count_until_server src/count_until_server.cpp)
ament_target_dependencies(count_until_server rclcpp rclcpp_action my_robot_interfaces)
add_executable(count_until_client src/count_until_client.cpp)
ament_target_dependencies(count_until_client rclcpp rclcpp_action my_robot_interfaces)

install(TARGETS
    test_node
    number_publisher
    number_counter
    count_until_server
    count_until_client
    DESTINATION lib/${PROJECT_NAME}/
)
...

colcon build --packages-select my_cpp_pkg
 -> action client를 build한다.
Starting >>> my_cpp_pkg
Finished <<< my_cpp_pkg [4.51s]                     
                     
Summary: 1 package finished [4.65s]

2개의 서로 다른 terminal을 띄운 상태에서 앞서 만든 action server와 client를 구동시켜 보자. 
----------------------------------
<Terminal#1>
$ source ~/.bashrc
$ ros2 run my_cpp_pkg count_until_server
[INFO] [1774588909.105287829] [count_until_server]: Action server has been started.
[INFO] [1774588926.456146363] [count_until_server]: Received a goal
[INFO] [1774588926.456272824] [count_until_server]: Accepting the goal
[INFO] [1774588926.456825680] [count_until_server]: Executing the goal
[INFO] [1774588926.456912297] [count_until_server]: 1
[INFO] [1774588926.957613919] [count_until_server]: 2
[INFO] [1774588927.457555858] [count_until_server]: 3
[INFO] [1774588927.957560879] [count_until_server]: 4
[INFO] [1774588928.456979643] [count_until_server]: 5

<Terminal#2>
$ source ~/.bashrc
$ ros2 run my_cpp_pkg count_until_client
[INFO] [1774588926.456922316] [count_until_client]: Goal got accepted
[INFO] [1774588926.457454104] [count_until_client]: Got feedback: 1
[INFO] [1774588926.958534711] [count_until_client]: Got feedback: 2
[INFO] [1774588927.458325103] [count_until_client]: Got feedback: 3
[INFO] [1774588927.958358714] [count_until_client]: Got feedback: 4
[INFO] [1774588928.457917311] [count_until_client]: Got feedback: 5
[INFO] [1774588928.958405625] [count_until_client]: Succeeded
[INFO] [1774588928.958596624] [count_until_client]: Result: 5

OK, (앞서 그림을 통해 소개한 것과 같이) 원하는 action 동작이 제대로 이루어지고 있음을 알 수 있다. 😎

10.7 Parameter 설정과 Launch System
우리는 지금까지 ROS 2 node 구현과 관련하여 아주 기본적인 사항(topic, service, action)만을 알아 보았다. 하지만 (기본편임에도 불구하고) 이 밖에도 추가로 알아야 할 내용이 더 있다. 😂

1) Parameter 설정 변경
 - Node의 설정 값을 run-time에 변경할 수 있도록 하는 기능을 말한다.
 - parameter는 명령행 option 형태로 지정할 수도 있고, YAML file에 (여러개의 파라미터 값을) 넣어서 설정할 수도 있다.
 - 만일 node가 실행된 상태에서 paramter 변경 요청이 들어갈 경우라면, 이를 node 내에서 반영하기 위해서는 반드시 parameter callback 함수를 구현해 주어야 한다.

[그림 10.7.1] number_publisher.cpp에 parameter callback 관련 코드 추가 모습

2) Launch system
 - 여러 개의 node와 parameter 설정을 한번에 하기 위해 등장한 개념이다.
 - Launch file 자체는 XML, YAML or Python file 형태로 만들 수 있다(여기에서는 가장 쉬운 XML을 기준으로 설명을 진행한다).
 - (요약) 별도의 package를 만들고, 그 내부에 lauch file을 추가한 후, colcon build 후 ros2 launch 명령을 사용하여 여러 개의 node를 한번에 실행한다.

이제 부터 Lauch file을 하나 만들어, 3개의 node를 한번에 구동하는 예를 소개해 보도록 하자.

[그림 10.7.2] launch file을 이용하여 3개의 node를 구동하는 예

ros2 pkg create my_robot_bringup --build-type ament_cmake
 -> launch file을 담는 전용 package를 하나 만든다.

cd my_robot_bringup/
$ ls -la
-rw-rw-r-- 1 chyi chyi  909  3월 29 16:04 CMakeLists.txt
drwxrwxr-x 3 chyi chyi 4096  3월 29 16:04 include
-rw-rw-r-- 1 chyi chyi  600  3월 29 16:04 package.xml
drwxrwxr-x 2 chyi chyi 4096  3월 29 16:04 src

rm -rf include/ src/
mkdir launch
$ cd launch/
vi number_app.launch.xml
 -> XML 형태로 된 lauch file을 하나 만든다.
<launch>
    <node pkg="my_cpp_pkg" exec="number_publisher" name="num_pub1">
        <remap from="/number" to="/my_number" />
    </node>
    <node pkg="my_cpp_pkg" exec="number_publisher" name="num_pub2">
        <remap from="/number" to="/my_number" />
    </node>
    <node pkg="my_cpp_pkg" exec="number_counter">
        <remap from="/number" to="/my_number" />
    </node>
</launch>

$ cd ..
$ vi package.xml    //아래 주황색 라인을 추가한다.
...
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <exec_depend>my_cpp_pkg</exec_depend>
...

$ vi CMakeLists.txt   //아래 주황색 라인을 추가한다.
...
find_package(ament_cmake REQUIRED)
install(DIRECTORY
    launch
    DESTINATION share/${PROJECT_NAME}/
)
...
ament_package()
~

$ cd ../..
$ colcon build --packages-select my_robot_bringup
 -> Launch file용 package를 build한다.
Starting >>> my_robot_bringup
Finished <<< my_robot_bringup [1.09s]                

Summary: 1 package finished [1.24s]

$ source ~/.bashrc
$ ros2 launch my_robot_bringup number_app.launch.xml
 -> launch file을 이용하여 3개의 node를 한번에 실행한다.
[INFO] [launch]: All log files can be found below /home/chyi/.ros/log/2026-03-29-17-05-12-522908-earth-new-4428
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [number_publisher-1]: process started with pid [4431]
[INFO] [number_publisher-2]: process started with pid [4432]
[INFO] [number_counter-3]: process started with pid [4433]
[number_publisher-1] [INFO] [1774771512.682481345] [num_pub1]: Number publisher has been started.
[number_publisher-2] [INFO] [1774771512.683688981] [num_pub2]: Number publisher has been started.
[number_counter-3] [INFO] [1774771512.685657584] [number_counter]: Number Counter has been started.
[number_counter-3] [INFO] [1774771513.683422361] [number_counter]: Counter: 2
[number_counter-3] [INFO] [1774771513.684439843] [number_counter]: Counter: 4
[number_counter-3] [INFO] [1774771514.683515722] [number_counter]: Counter: 6
[number_counter-3] [INFO] [1774771514.684471167] [number_counter]: Counter: 8
[number_counter-3] [INFO] [1774771515.683616659] [number_counter]: Counter: 10
[number_counter-3] [INFO] [1774771515.684577615] [number_counter]: Counter: 12
[number_counter-3] [INFO] [1774771516.683717991] [number_counter]: Counter: 14
[number_counter-3] [INFO] [1774771516.684468825] [number_counter]: Counter: 16

위의 명령 실행 후, process 상태(ros2 launch 관련 1개의 python script와 3개의 C++ node)를 확인해 보면 다음과 같다.

$ ps aux|grep ros2
chyi        4428  2.8  0.2 199084 41984 pts/0    Sl+  17:05   0:00 /usr/bin/python3 /opt/ros/jazzy/bin/ros2 launch my_robot_bringup number_app2.launch.xml
chyi        4431  0.5  0.1 677816 27820 pts/0    Sl+  17:05   0:00 /mnt/hdd/workspace/ROS2/ros2_ws/install/my_cpp_pkg/lib/my_cpp_pkg/number_publisher --ros-args -r __node:=num_pub1 -r /number:=/my_number
chyi        4432  0.5  0.1 677940 28236 pts/0    Sl+  17:05   0:00 /mnt/hdd/workspace/ROS2/ros2_ws/install/my_cpp_pkg/lib/my_cpp_pkg/number_publisher --ros-args -r __node:=num_pub2 -r /number:=/my_number
chyi        4433  0.5  0.1 680796 29604 pts/0    Sl+  17:05   0:00 /mnt/hdd/workspace/ROS2/ros2_ws/install/my_cpp_pkg/lib/my_cpp_pkg/number_counter --ros-args -r /number:=/my_number
chyi        4478  0.0  0.0   5008  2372 pts/1    S+   17:05   0:00 grep --color=auto ros2

OK, 여기까지 launch file을 이용하여 여러 개의 node를 한번에 구동시키는 방법까지 확인해 보았다. 😎

10.8 Cross-Compile 환경 고려하기
실제 target board(STM32MP257F-DK) 환경에 맞게 ROS 2 node를 cross compile해 주어야 하는데, 이와 관련한 내용을 정리해 보면 다음과 같다.

a) ROS 2 C++ 기반 Node cross-compile하기
colcon은 cmake 기반의 build를 위한 wrapper에 불과하다. 따라서 cmake 환경에서의 cross-compile 방법에 익숙한 독자라면, 어렵지 않게 cross compile을 수행할 수가 있다. 관련해서는 아래 link가 도움이 될 것 같다.


📌 당연한 거지만, Python 기반 Node는 cross-compile 과정이 필요 없다.

b) ROS 2 C++ 기반 Node yocto 환경에서 build하기
Yocto 환경에서 ROS 2 node를 compile하기 위해서는 절차가 까다롭게 느껴질 수가 있다. 이와 관련해서는 이번 posting 6장을 참조해 주기 바란다.


-----------------------------------------------------------
이상으로 (ROS 2의 가장 기본 중의 기본인) ROS 2 node와 주요 통신 방식인 topic, service, action에 관하여 간단히 살펴 보았다. 또한 node 파라미터 설정 방법과 Launch system에 관해서도 시험해 보았다.

그런데, 지금까지 설명한 내용만 가지고 과연 실질적인 Robot이나 자율주행 차량 혹은 드론 같은 것을 만드는 것이 가능할까 ? 당연히 안된다. 그렇다면 어찌해야 하는 것일까 ?

그에 대한 답은 아래와 같은 보다 깊은 주제(ROS 2 advanced)를 통해서 찾을 수가 있다. 헐, 갈수록 태산이다 ! Let's see this through to the end. 😋 

<Next step에서 study해야 할 내용>
1) 3D robot modeling with URDF & RViz and Simulating Robot with Gazebo
 -> 보통은 여기까지를 기본편에서 다루고 있긴하다.

<ROS 2 advanced>
2) ros2_control - 실시간 h/w 제어 framework
3) Nav2 - 자율주행 및 Navigation stack
4) MoveIt2 - Robot manipulation & motion planning(다관절 로봇 Arm 관련)
5) Perception Stack - computer vision & image procesing
6) LLM with ROS2 - ROS2와 대규모 언어 모델 연동
7) Deep Reinforcement Learning with ROS2 - 심층 강화 학습
...


To be continued...


References
[1] https://www.st.com/en/evaluation-tools/stm32mp257f-dk.html
[2] UM3385 - Discovery kit with STM32MP257F MPU - User manual, STMicroelectronics.
[3] Data brief - STM32MP257F-DK, STMicroelectronics.
[4] STM32MP251C/F STM32MP253C/F, STM32MP255C/F STM32MP257C/F Datasheet, STMicroelectronics.
[5] STM32MP257-DK schematic, STMicroelectronics.
[6] RM0457 Reference manual, STM32MP23/25xx advanced Arm®-based 32/64-bit MPUs
[7] https://wiki.st.com/stm32mpu/wiki/Getting_started/STM32MP2_boards/STM32MP257x-DK
[8] https://wiki.st.com/stm32mpu/wiki/STM32MP2_boot_chain_overview
[9] https://wiki.st.com/stm32mpu/wiki/STM32MPU_Distribution_Package
[10] https://www.st.com/en/microcontrollers-microprocessors/stm32mp257f.html
[11] https://wiki.st.com/stm32mpu/wiki/Linux_remoteproc_framework_overview#Remote_processor_boot_through_sysfs
[12] https://wiki.st.com/stm32mpu/wiki/Category:Buildroot-based_Linux_embedded_software
  -> STM32MP documents

[13] https://www.ros.org/imgs/ROSBrandGuide.pdf
[14] https://docs.ros.org/en/humble/index.html
[15] https://docs.ros.org/en/kilted/index.html
[16] ROS2 from Scratch, Get started with ROS 2 and create robotics applications
with Python and C++, Edouard Renard, Packt
[17] PROGRAMMING FOR AUTONOMOUS SYSTEMS, Fred Livingston, PhD
[18] https://micro.ros.org/
[19] https://docs.ros.org/en/rolling/Concepts/Advanced/About-Internal-Interfaces.html
[20] https://category.yahboom.net/collections/ros-robotics
 -> ROS 2

[21] And Google and Gemini~

Slowboot

댓글 없음:

댓글 쓰기