我們已經知道,在ROS2中,節點是相當重要的,所以這裏我們將講解一個簡單的節點是如何構建的
節點的構建
首先簡單介紹一下RCLCPP和RCLPY:
這兩個作為頭文件或者庫文件在我們的功能包中往往是相當常見的,分別應用於C++和Python編寫的節點,作為客户端庫。在其中,會提供相應的通訊接口和相應模塊,方便我們對系統的構建
在我們使用的這個庫中就已經有對於結點(Node)的定義,所以我們可以直接調用這裏面的函數來創建我們具有多通訊功能的節點
這裏我們可以看到我們的節點具有很多已經封裝好的方法,一口氣學完是不可能的,所以我們在後續通訊方式的學習中取用需要的內容進行學習即可
下面先後給出cpp和py的實例,再對代碼的細節做具體的解釋:
(這裏先不要跟着抄代碼和運行,我們先看基本的結構)
C++:
#include "rclcpp/rclcpp.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<rclcpp::Node>("node_01");
RCLCPP_INFO(node->get_logger(),"hello world!");
rclcpp::spin(name);
rclcpp::shutdown();
return 0;
}
- 調用我們的頭文件對於 rclcpp 文件的引用進行部署
- main()中傳入參數 argc 和 argv ,其中 argv 相對重要,作為命令行中傳入的參數值,從1開始(因為 0 被本身佔位),每一個參數以 char 類型呈現,可以通過函數進行轉換
- rclcpp::init() 主要對於後續使用的節點內通訊工具進行初始化部署
- auto為自動類型推導,make_shared 為共享指針,括號中傳入的是節點名稱
- RCLCPP_INFO 是節點發送信息的一種類型,node->get_logger()調用了節點發送信息的日誌,後面傳入的就是發送信息的內容
- rclcpp::spin(name) 保持節點運行,檢測是否有輸入退出指令(Ctrl+C)
- rclcpp::shutdown() 關閉
Python:
import rclpy
def main(args=None):
rclpy.init(args)
node = rclpy.create_node("node_02")
node.get_logger().info("hello world!")
rclpy.spin(node)
rclpy.shutdown()
- import rclpy 導入 rclpy 的一些方法
- def main() 設置函數 main()(後面會作為接口函數來使用,這裏先不做過於詳細的解釋,後面配置文件再具體説明)
- rclpy.init() 對於節點的通訊接口等進行初始化
- create_node 同樣是創建節點,同時設置節點名
- get_logger().info(...) 同樣是調用了日誌,進行信息發送
- rclpy.spin(node) 保持節點運行,檢測是否有退出指令(Ctrl+C)
- rclpy.shutdown() 關閉節點
可以看到的是,在上面的節點實例中,節點的構造邏輯和結構基本是一致的
節點的基礎文件配置:
我們的節點編譯需要相應的文件結構,根據前文給出的介紹,如果要對節點進行構建,需要先構建工作空間,再有功能包,最後才是節點
下面我們分 cpp 和 py 講解文件的結構:
CPP文件結構
先創建一個文件夾:
mkdir -p ros2_learning/node_learning/
cd ros2_learning/node_learning
mkdir -p node_demo_ws/src/
在上面的文件結構中,/ros2_learning 是我們學習用的一個大文件包,這裏面分出了不同的學習內容,今天學習node的創建,所以我們使用 /node_learning,在此之下創建工作空間 /node_demo_ws,然後創建源文件的文件夾 /src
接下來我們使用ros2內置的包創建方法來創建cpp功能包結構
cd node_demo_ws/src
ros2 pkg create node_example_cpp --build-type ament_cmake --dependencies rclcpp
這裏跳轉到 /src 後,我們調用 pkg create,創建了名為 node_example_cpp 的功能包,類型為 ament_cmake ,並添加了 rclcpp 的依賴
在我們的功能包創建完成後,我們以/src為根的文件目錄如下:
.
└── src
└── node_example_cpp
├── CMakeLists.txt
├── include
│ └── node_example_cpp
├── package.xml
└── src
5 directories, 2 files
接下來,我們在 src 目錄下創建 node_01.cpp 文件,文件結構就變成了這樣:
.
└── src
└── node_example_cpp
├── CMakeLists.txt
├── include
│ └── node_example_cpp
├── package.xml
└── src
└── node_01.cpp
5 directories, 3 files
這個時候就可以把我們的節點代碼寫入 node_01.cpp 中了
這個時候還不能直接運行,需要先修改 cmake 文件和 package 文件來添加文件路徑和相應依賴:
CMakeList.txt
這個文件的名稱是不能改變的,可以簡單閲讀裏面的內容,可以發現有許多類似於提供依賴的語句,這個後面再來細講
我們先走到CMakrList的最後一句前面,加入如下兩段代碼:
add_executable(node_01 src/node_01.cpp)
ament_target_dependencies(node_01 rclcpp)
上面兩段代碼的含義:
- add_executable([node_name] [file_list]) 通過文件結合生成可執行的節點文件
- amrnt_target_dependencies([node_name] [dependencies_list]) 向節點添加需要的依賴列表
install(TARGETS
node_01
DESTINATION lib/${PROJECT_NAME}
)
上面的代碼含義:
表示安裝 node_01 這個可執行文件(目標),其中 &{PROJECT_NAME} 作為 CMake 的內置變量,表示當前項目的名稱
這裏 package.xml 不需要做其他的修改,保持功能包生成時的原樣即可
如何編譯運行
我們需要依次運行下面三段命令行(注意運行的時候命令行所處的位置應該是 /node_demo_ws):
colcon build
source install/setup.bash
ros2 run node_example node_01
- colcon build 表示對於當前工作空間下的所有文件進行編譯
- source install/setup.bash 用於加載當前工作空間的環境變量,可以保證 ros2 在編譯後能對你的其他命令進行識別
- ros2 run [package] [node] 表示對於某個功能包中的某個可執行節點進行運行
注意:
- source install/setup.bash 在每次編譯之後最好都要重新操作一遍,防止出現奇怪的問題
接下來你就可以在命令行輸入ros2 node list來查看現在正在運行的所有節點
現在再看你的文件結構,會發現工作空間下多了三個冒昧的傢伙:/log、/build 和 /install
如果某次你的代碼出現了玄學錯誤,那麼可以嘗試刪除這三個冒昧的傢伙,有可能代碼的功能就正常了(
Py文件結構:
我們的文件結構在上方時相近的,所以我們可以依賴上面CPP中創建的文件結構繼續構建 py 的結構:
cd /node_demo_ws/src/
ros2 pkg create node_example_py --build-type ament_python --dependencies rclpy
這裏的第二個語句相信你可以自己解釋了
接下來我們能看到如下的文件結構:
.
├── node_example_py
│ └── __init__.py
├── package.xml
├── resource
│ └── node_example_py
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 8 files
接下去我們在 node_example_py 之下創建 node_02.py,文件結構如下:
.
├── node_example_py
│ ├── __init__.py
│ └── node_02.py
├── package.xml
├── resource
│ └── node_example_py
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 9 files
在 node_02.py 中,我們就可以加入我們的節點代碼了
接下來配置 setup.py 文件(修改如下部分):
entry_points={
'console_scripts': [
"node_02 = node_example_py.node_02:main"
],
},
)
這裏聲明瞭一個功能包下的節點(等號右邊的結構),名為 node_02(等號左邊那個),同時提供了入口函數為 main 函數
這相當於為 colcon build 提供了一個固定的路徑,編譯的時候文件方向就能明確了
接下來我們編譯運行我們的 python 節點(注意同樣需要在WorkSpace中運行,不要走錯路徑):
colcon build
source install/setup.bash
ros2 run node_example_py node_02
這上面的三句話已經很明確了,這裏就不多解釋了
你同樣可以使用ros2 node list來對運行中的節點進行查看
繼承的方式構築節點
我們在真實的應用場景中,往往不會使用上面的方法對節點進行創建,因為RCL提供的原裝節點Node類具有的功能基本只有基礎的通訊,無法很好的滿足我們的實際應用需求,在前面我們也介紹了在我們的模塊化編程中,OOP(面向對象編程)是一種相當重要的思想,所以我們這裏需要使用繼承的方法在Node的基礎上創建自己的新節點
接下來同樣先展示代碼,再解釋代碼內容:
C++:
#include "rclcpp/rclcpp.hpp"
class MyNode01 : public rclcpp::Node {
public:
MyNode01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger,"Hello,I'm %s",name.c_str());
}
private:
};
int main(int argc,char **agrv) {
rclcpp::init(argc,argv);
auto node=std::make_shared<MyNode01>("mynode_01");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- 上面共享指針的類型變成了 MyNode01
- MyNode01 繼承自公共的 Node 類型,並調用構造函數向 Node 提供 name 參數
- 這裏在構造函數中的 this 用於指向自身,不寫也是不會報錯的,但是寫上結構會更清晰,方便後續迭代
- 這裏一個小細節,我們的正則表達式中 %s 表示的是c類型的字符串,所以我們這裏需要用 .c_str() 來進行風格轉換(當然,不寫也只是報一個 WARNING)
其餘文件配置和編譯的過程與之前都是一樣的,只是如果你不想把一個大文件夾中的所有文件同時編譯的話,可以在編譯的時候添加參數如下:
colcon build --packages-select [package_list]
這樣可以僅針對某些功能包進行編譯
Python:
import rclpy
from rclpy.node import Node
class MyNode02(Node):
def __init__(name):
super.__init__(name)
self.get_logger().info("大家好,我是%s!" % name)
def main(args=None):
rclpy.init(args=args)
node = MyNode02("mynode_02")
rclpy.spin(node)
rclpy.shutdown()
- class 對 MyNode02 繼承自Node類型進行定義
- MyNode02 定義構造函數,super.init() 調用父類構造函數傳入 name 參數
- self. 表示自己,和 C++ 中 this-> 的含義是一樣的
其餘的配置方法與先前是一致的
那麼相信你現在已經有自己創建自己的節點的能力了,接下來我們將進入ROS2四種主要通訊方式的學習