# ROS2入門: 建立Package
透過[1]提供的範例程式建立基礎的Package,有2個node,內部各有Publisher以及Subscriber互相傳Hello world。
# Step 1, 建立工作空間
建立資料夾ros2_ws/src,並進入到ros2/src。
使用ros2 pkg create建立colcon bulid需要的工作介面。
```
$ ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
# $ 格式: ros2 pkg create --build-type ament_cmake --license <licnese> --dependencies <dep 1><dep 2><dep 3>... <pkgname>
```
這時候會建立好資料夾結構。
```
(~/ros2_ws)
.
|-- cpp_pubsub
|-- include
| |-- cpp_pubsub
|-- src
|-- package.xml
|-- CMakeLists.txt
```
之後要把範例程式放在資料夾src下面。進入~/ros2_ws/src/cpp_pubsub/src編輯範例程式。
# Step 2, 下載publisher的範例程式
## Step 2-1, 下載publisher範例程式
將publisher_member_function.cpp下載到ros2_ws/src/cpp_pubsub/src。
```
wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp
```
解讀程式碼在本章節(Step 2)最後。
## Step 2-2, 修改package.xml CMakeLists.txt加入編譯需要的資訊
符合colcon build格式,需要編輯package.xml,CMakeLists.txt,在工作區位置的:ros2_ws/src/cpp_pubsub/
### 編輯package.xml
這個檔案紀錄package的詳細資料,也會記錄使用到的dependencies。這個package使用到的dependencies有兩個:rclcpp、std_msgs,在ament_cmake後面加入:
```xml=
<depend>rclcpp</depend>
<depend>std_msgs</depend>
```
### CMakeLists.txt
CMakeLists管理編譯需要的package以及編譯成哪個應用程式。
**注意: 使用ament_cmake一定要在開頭加入ament_cmake這個package**
在find_package(ament_cmake REQUIRED)之後加入find_package(*dependency name* REQUIRED):
```cmake=
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
```
然後加入add_executable(), 這裡要命名編譯完的executable。
加入ament_target_dependencies()表示編譯這個executable需要用到的dependencies為rclcpp stdmsgs。
```cmake=
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
# 格式: add_executable(*name* *source code location*)
```
加入install,之後在ros2 run時就可以找到這個executable名稱。
```cmake=
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
```
加入完之後完成publisher需要的編譯設定。
最後要用ament_package()結尾。這一步會安裝package.xml、將package註冊到ament index、安裝CMakeConfig。
根據[1],完整CMakeLists.txt:
```cmake=
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
```
## 補充資料
1. 非ROS2 library
根據[2],使用ROS2 package時ament自動找ROS2 dependencies下面的package;非ROS2的dependencies需要自己指定路徑給compiler,使用方式參考:
```
include_directories(
include
${INCLUDE_DIRS ... }
${INCLUDE_DIRS ... })
```
並使用target_link_libraries連接。
```
target_link_libraries(${PROJECT_NAME}_node
helper
${PCL_LIBRARIES}
${OpenCV_LIBRARIES}
${Eigen3_LIBRARIES}
)
```
2. build成library
根據[2],使用add_library()而非add_executable()來build。使用方式參考:
```
add_library(*lib name* *source code location*)
```
## 解讀程式碼:
解讀程式碼參考[1, 2, 3]。
1. 加入rclcpp dependencies
```cpp=
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
```
inculde libraries加入ROS2以及ROS2 msgs需要的library。
2. Constructor以及初始化
```cpp=
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
```
新的node是繼承自rclcpp::Node這個物件的物件。
實體化publisher回傳給publisher_,message type設定為String(std_msgs::msg::String),命名為topic,設定queue為10(可以存10筆資料)。
Publisher_, timer_, count_都是最後會定義在private的member variable,最後會宣告在private區,在Constructor內部需要做初始化。
實體化wll_timer回傳給timer_,設定以500ms為period啟動自己的member function timer_callback,timer_callback是message實際上送出的function,後續會設定timer_callback的內容。
3. Member function timer_callback()
```cpp=
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
```
真的送出message的member function。RCLCPP_INFO為在console印出資訊的macro。
建立並初始化message物件,設定內容是Hello, world!加入count_,count要轉string;這之後在console印出資訊,注意message.data要轉c_str。
最後讓publisher_送出。
C++的auto根據初始化方式決定type,使用方式如[4]。
4. Member variable
```cpp=
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
```
這個Node需要用到的Member variable定在Member functions後面。
5. main function
```cpp=
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
```
main function首先initialize ROS2(rclcpp::init(...))。
用rclcpp::spin(...)啟動Node。
# Step 3, 下載subscriber的範例程式
與Step 2相似。
## Step 3-1, 下載subscriber範例程式
將subscriber_member_function.cpp下載到ros2_ws/src/cpp_pubsub/src。
```
wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp
```
這章節最後解析程式碼。
## Step 3-2, publisher修改CMakeLists.txt加入編譯需要的資訊
package.xml資訊相同,沒有新增dependencies。
有加入新的cpp之後、且要編成新的executable file,要修改CMakeLists.txt。
CMakeLists.txt需要修改這些項目,加入一個新的executable為listener:
add_executable()
ament_target_dependencies()
install()
改成:
```cmake=
...
add_executable(listener src/subscriber_member_function.cpp)
...
ament_target_dependencies(listener rclcpp std_msgs)
...
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
...
```
## 解讀程式碼:
解讀程式碼參考[1, 2, 3]。(與Publisher相似的部分省略。)
1. Constructure以及初始化
```cpp=
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
```
subscription_是member variable,型別為指標,實體化一個接收String的subscription給subscriptiton_,並解bind到member function topic_callback。Topic名稱為"topic",queue為10。
**注意: Publisher/Subscriber架構中topic名稱必須相同。**
2. Member function
```cpp=
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
```
topic_callback收到topic之後啟動,會用RCLCPP_INFO marco印出資訊到console。
:::info
補充說明: void const func(){} 和 void func() const {}。
1. *type* const func()和*type* void func()相同,其ruturn type為const。
2. void func() const {} 表示這個function不能呼叫非const function、不能改變variable、const物件只能呼叫const函數。
Refer to [5]。
:::
# 編譯ROS2 Package
不確定dependencies rclcpp std_msgs是否已經安裝,可以用rosdep檢查。
```
rosdep install -i --from-path src --rosdistro humble -y
```
顯示#All required rosdeps installed successfully表示需要的dependencies都安裝成功
:::info
遇到terminal報Error your rosdep installation has not been initialized yet。用以下方式更新:
```
sudo rosdep init
rosdep update
```
:::
接下來使用colcon build來編譯ROS2 package:
```
colcon build --packages-select cpp_pubsub
```
會開始編譯--packages-select後面選的package(s)。
# 執行ROS2 node(s)
這個Package下面有兩個node,先source ~/ROS2_ws/install/setup.bash,成功載入環境變數後,可以讀到package cpp_pubsub下面有talker, listener兩個node。
執行方式:
```
(Terminal 1)
$ ros2 run cpp_pubsub talker
(Terminal 2)
$ ros2 run cpp_pubsub listener
```
執行成功可以看到talker一直對listener丟字串 Hello, world!
# 移除package
根據colcon workspace,移除package需要下列步驟
1. 移除package原始檔
```
rm -rf src/<package_name>
```
2. 移除已經產生過的build, install, log
```
rm -rf build/ install/ log/
```
3.重新產生環境
```
colcon build
```
# 參考資料
[1][ROS Publisher/Subscriber範例說明官方文件](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html#create-a-package)
[2]ROS2入門中文說明,解釋較多CMakeLists.txt內容:https://ithelp.ithome.com.tw/articles/10318211
[3]ROS2入門中文說明:https://ithelp.ithome.com.tw/articles/10332960
[4][Type Inference in C++ (auto and decltype)](https://www.geeksforgeeks.org/type-inference-in-c-auto-and-decltype/)
[5][General C++ Programming void const f() vs void f() const](https://cplusplus.com/forum/general/12087/)