# 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/)