Wiki
ROS2其他通訊機制
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.概述



不同裝置之間的通訊,是通過分散式來實現的。



映象王八



2.分散式搭建
場景
在許多機器人相關的應用場景中都涉及到多臺ROS2裝置協作,比如:無人車編隊、無人機編隊、遠端控制等等,那麼不同的ROS2裝置之間是如何實現通訊的呢?
概念
分散式通訊是指可以通過網路在不同主機之間實現資料互動的一種通訊策略。
ROS2本身是一個分散式通訊框架,可以很方便的實現不同裝置之間的通訊,ROS2所基於的中介軟體是DDS,當處於同一網路中時,通過DDS的域ID機制(ROS_DOMAIN_ID)可以實現分散式通訊,大致流程是:在啟動節點之前,可以設定域ID的值,不同節點如果域ID相同,那麼可以自由發現並通訊,反之,如果域ID值不同,則不能實現。預設情況下,所有節點啟動時所使用的域ID為0,換言之,只要保證在同一網路,你不需要做任何配置,不同ROS2裝置上的不同節點即可實現分散式通訊。
作用
分散式通訊的應用場景是較為廣泛的,如上所述:機器人編隊時,機器人可能需要獲取周邊機器人的速度、位置、執行軌跡的相關資訊,遠端控制時,則可能需要控制端獲取機器人採集的環境資訊並下發控制指令...... 這些資料的互動都依賴於分散式通訊。
實現
多機通訊時,可以通過域ID對節點進行分組,組內的節點之間可以自由通訊,不同組之間的節點則不可通訊。如果所有節點都屬於同一組,那麼直接使用預設域ID即可,如果要將不同節點劃分為多個組,那麼可以在終端中啟動節點前設定該節點的域ID(比如設定為6),具體執行命令為:
export ROS_DOMAIN_ID=6
上述指令執行後,該節點將被劃分到ID為6的域內。
如果要為當前裝置下的所有節點設定統一的域ID,那麼可以執行如下指令:
echo "export ROS_DOMAIN_ID=6" >> ~/.bashrc
執行完畢後再重新啟動終端,執行的所有節點將自動被劃分到ID為6的域內。

預設域ID是0,域ID不一樣,無法互相通訊。


ID不一樣就無法正常通訊,和在ROS1內需要指定ROS Master一樣。
注意
在設定ROS_DOMAIN_ID的值時並不是隨意的,也是有一定約束的:
-
建議ROS_DOMAIN_ID的取值在[0,101] 之間,包含0和101;
-
每個域ID內的節點總數是有限制的,需要小於等於120個;
-
如果域ID為101,那麼該域的節點總數需要小於等於54個。
DDS 域 ID 值的計算規則
域ID值的相關計算規則如下:
-
DDS是基於TCP/IP或UDP/IP網路通訊協議的,網路通訊時需要指定埠號,埠號由2個位元組的無符號整數表示,其取值範圍在[0,65535]之間;
-
埠號的分配也是有其規則的,並非可以任意使用的,根據DDS協議規定以7400作為起始埠,也即可用埠為[7400,65535],又已知按照DDS協議預設情況下,每個域ID佔用250個埠,那麼域ID的個數為:(65535-7400)/250 = 232(個),對應的其取值範圍為[0,231];
-
作業系統還會設定一些預留埠,在DDS中使用埠時,還需要避開這些預留埠,以免使用中產生衝突,不同的作業系統預留埠又有所差異,其最終結果是,在Linux下,可用的域ID為[0,101]與[215-231],在Windows和Mac中可用的域ID為[0,166],綜上,為了相容多平臺,建議域ID在[0,101] 範圍內取值。
-
每個域ID預設佔用250個埠,且每個ROS2節點需要佔用兩個埠,另外,按照DDS協議每個域ID的埠段內,第1、2個埠是Discovery Multicast埠與User Multicast埠,從第11、12個埠開始是域內第一個節點的Discovery Unicast埠與User Unicast,後續節點所佔用埠依次順延,那麼一個域ID中的最大節點個數為:(250-10)/2 = 120(個);
-
特殊情況:域ID值為101時,其後半段埠屬於作業系統的預留埠,其節點最大個數為54個。
上述計算規則瞭解即可。
域 ID 与节点所占用端口示意
Domain ID: 0
Participant ID: 0
Discovery Multicast Port: 7400
User Multicast Port: 7401
Discovery Unicast Port: 7410
User Unicast Port: 7411
---
Domain ID: 1
Participant ID: 2
Discovery Multicast Port: 7650
User Multicast Port: 7651
Discovery Unicast Port: 7664
User Unicast Port: 7665
Domain ID是指域ID
Participant ID是參與組ID,是指該域內的第幾個節點
Discovery Multicast Port主發現埠,DDS規定,埠應從7400開始
User Multicast Port使用者廣播埠,
Discovery Unicast Port單播發現埠,7410是第一個節點開始使用的Discovery Unicast埠,因為DDS規定的,第11個端口才是第一個節點的Discovery Unicast埠。
User Unicast Port單播使用者埠,7411才是第一個節點開始使用的User Unicast埠,因為DDS規定的,第12個端口才是第一個節點的User Unicast埠。
Discovery Unicast Port和User Unicast Port是第一個節點所佔用的埠,所以一共佔用了倆埠。
如果Domain ID不變還為0,Participant ID變成1的話,那麼下一個節點的Discovery Unicast Port為7412,User Unicast Port為7413。

以此類推。
一個Domain ID佔用250個埠,所以當Domain ID為1的時候,Discovery Unicast Port應該是7650。
這是第三個節點,所以是7664和7665。

實踐:在樹莓派5上跑一個ROS2 Jazzy開啟鍵盤控制節點,然後在電腦實體機Linux(或者在Docker裡跑也行,但要設定好網路)裡跑一個ROS2 Humble,並開啟烏龜顯示節點。

會發現是可以正常通訊的。
3.工作空間覆蓋


沒什麼用,建議不要使用



這個是和你的bashrc檔案裡,載入bash檔案的順序有關,誰最後載入,誰就會執行,也就是最高優先順序。除了ROS2本身自帶的bash,這個bash是不論在哪載入,都是最低優先順序。



4.元功能包


distro就是發行版的意思


--build-type預設C++,
--dependent預設無,
--node-name本身是虛包,所以也無需設定。

CmakeLists無需修改

先把12,13行刪除
<exec_depend>xxxxxx</exec_depend>
所要依賴的功能包名



以後可能需要的元功能包:

這個功能包是導航相關的

這個就是個元功能包

只有配置檔案,沒有其他實質性實現



元功能包的作用就是方便安裝,把自己的東西打包,可以共享到ROS2社群。也方便安裝別人的東西。
5.節點重名


而且節點名稱都是一致的,圖中有個<2>,這是作業系統給的標號,其他的作業系統是沒有這個標號的。

rqt_graph

他只顯示一個turtlesim,實際上我們是使用了兩個turtlesim的。

這樣雖然都顯示了,但是是一模一樣的名字,容易混淆,而且上面也給重名警告了。



要麼起別名:王大寶,王小寶
要麼加名稱空間: 毛驢子家的王寶,李二狗家的王寶



ros2 run turtlesim turtlesim_node --ros-args --remap __ns:=/t1
ros2 run 功能包名 節點名 --ros-args --remap __ns:=/名稱空間


ros2 run turtlesim turtlesim_node --ros-args --remap __name:=turtlesim2
ros2 run 功能包名 節點名 --ros-args --remap __name:=別名
or
ros2 run 功能包名 節點名 --ros-args --remap __node:=別名



ros2 pkg create cpp05_names --build-type ament_cmake --node-name demo01_names
節點名可以不設定,這裡設定主要是為以後學習做鋪墊。



install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})


先導倆包

這個LaunchDescription物件裡面呢是個列表,這個列表就是儲存要啟動的若干個節點。


from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package="turtlesim",executable="turtlesim_node",name="t1")
])
package功能包名
executable節點名
name別名

ros2 launch cpp05_names demo01_names_launch.py




根標籤是Launch
子集標籤是node

<launch>
<node pkg="turtlesim" exec="turtlesim_node" name="t1" />
</launch>

ros2 launch cpp05_names demo02_names_launch.xml

yaml的根標籤也是launch

launch:
node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "t1"

ros2 launch cpp05_names demo03_names_launch.yaml



name別名
namespace名稱空間

from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package="turtlesim",executable="turtlesim_node",name="turtle1"),
Node(package="turtlesim",executable="turtlesim_node",namespace="t1"),
Node(package="turtlesim",executable="turtlesim_node",namespace="t1",name="turtle1")
])

<launch>
<node pkg ="turtlesim" exec ="turtlesim_node" name ="turtle1" />
<node pkg ="turtlesim" exec ="turtlesim_node" namespace ="t1" />
<node pkg ="turtlesim" exec ="turtlesim_node" namespace ="t1" name= "turtle1" />
</launch>

launch:
node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "turtle"
node:
pkg: "turtlesim"
exec: "turtlesim_node"
namespace: "t1"
node:
pkg: "turtlesim"
exec: "turtlesim_node"
namespace: "t1"
name: "turtle"





可指定名稱空間

#include "rclcpp/rclcpp.hpp"
class MyNode: public rclcpp::Node
{
public:
MyNode():Node("mynode_node_cpp","t1_ns")
{
RCLCPP_INFO(this->get_logger(),"Hello World!");
}
};
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}




6.話題重名





ros2 run turtlesim turtlesim_node --ros-args --remap __ns:=/t1
ros2 run 功能包名 節點名 --ros-args --remap __ns:=/名稱空間
這種加名稱空間的方式,不僅對節點重名生效,當然對話題名稱依然生效。





ros2 run teleop_twist_keyboard teleop_twist_keyboard
這個是開啟控制機器人運動的節點
ros2 run teleop_twist_keyboard teleop_twist_keyboard


其所對應的話題名稱是這個。

但此時控制烏龜運動無效,是因為話題名稱空間不同。

烏龜接收的話題是/turtle1/cmd_vel,名稱空間是/turtle1
而控制烏龜運動的是/cmd_vel,名稱空間不同,
所以我們要把兩者名稱空間弄成一樣的。
隨便改即可,只要改成一樣的,就可以正常通訊了。



remappings可以實現話題的重對映,該引數是一個列表,然後裡面是元組,每一個元組都可以對一個話題進行重對映,元組裡第一個引數是原話題名稱,第二個引數是重對映後的話題名稱。
namespace可以實現名稱空間。








remp也是可以設定重對映,from是原話題名稱,to是重對映的名稱。
<launch>
<node pkg ="turtlesim" exec ="turtlesim_node" namespace ="t1" />
<node pkg ="turtlesim" exec ="turtlesim_node" >
<remap from= "/turtle1/cmd_vel" to="/cmd_vel" />
</node>
</launch>


launch:
- node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "turtle"
- node:
pkg: "turtlesim"
exec: "turtlesim_node"
namespace: "t1"
- node:
pkg: "turtlesim"
exec: "turtlesim_node"
namespace: "t1"
name: "turtle"
node:
pkg: "turtlesim"
exec: "turtlesim_node"
namespace: "t1"
node:
pkg: "turtlesim"
exec: "turtlesim_node"
remap:
-
from: "/turtle1/cmd_vel"
to: "/cmd_vel"




名稱空間可以有好幾級。

全域性話題是和節點名稱空間平級,也就是掛載在根下的。
相對話題是掛載在名稱空間下的。
私有話題是節點名稱的子級。




#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std_msgs::msg::String;
class MyNode: public rclcpp::Node
{
public:
MyNode():Node("mynode_node_name","t1_namespace")
{
RCLCPP_INFO(this->get_logger(),"Hello World!");
publisher_ = this->create_publisher<String>("/global_topics",10);
}
private:
rclcpp::Publisher<String>::SharedPtr publisher_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}



全域性話題


相對話題


私有話題
7.時間相關API

發訊息可以有訊息頭,訊息頭裡有時間戳,接收方解析訊息頭,並把訊息時間和當前時間進行比對,看是否延遲過高。
Rate是頻率
Time是時刻
Duration是持續時間





這個類的建構函式有兩個過載,第一個是週期,第二個是頻率。



#include "rclcpp/rclcpp.hpp"
using namespace std::chrono_literals;
class MyNode: public rclcpp::Node
{
public:
MyNode():Node("time_node_cpp")
{
RCLCPP_INFO(this->get_logger(),"Hello World!");
demo_rate();
}
private:
void demo_rate()
{
rclcpp::Rate rate1(500ms);
rclcpp::Rate rate2(1.0);
// while(rclcpp::ok())
// {
// RCLCPP_INFO(this->get_logger(),"休眠500ms");
// rate1.sleep();
// }
while(rclcpp::ok())
{
RCLCPP_INFO(this->get_logger(),"休眠1000ms");
rate2.sleep();
}
}
};
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}




可以傳入一個納秒

也可以傳入一個秒和一個納秒

因為是int64_t型別的,所以我們後面加個L,這是5億納秒,也就是0.5秒。

這樣time2代表2.5秒。
獲取當前時刻有兩種方式,
一個是this->get_clock()->now(),
另一個是this->now();




#include "rclcpp/rclcpp.hpp"
using namespace std::chrono_literals;
class MyNode: public rclcpp::Node
{
public:
MyNode():Node("time_node_cpp")
{
RCLCPP_INFO(this->get_logger(),"Hello World!");
// demo_rate();
demo_time();
}
private:
void demo_rate()
{
rclcpp::Rate rate1(500ms);
rclcpp::Rate rate2(1.0);
// while(rclcpp::ok())
// {
// RCLCPP_INFO(this->get_logger(),"休眠500ms");
// rate1.sleep();
// }
while(rclcpp::ok())
{
RCLCPP_INFO(this->get_logger(),"休眠1000ms");
rate2.sleep();
}
}
void demo_time()
{
rclcpp::Time time1(500000000L);
rclcpp::Time time2(2,500000000L);
rclcpp::Time right_now_1 = this->get_clock()->now();
rclcpp::Time right_now_2 = this->now();
RCLCPP_INFO(this->get_logger(),"s = %.2f , ns = %ld",time1.seconds(),time1.nanoseconds());
RCLCPP_INFO(this->get_logger(),"s = %.2f , ns = %ld",time2.seconds(),time2.nanoseconds());
RCLCPP_INFO(this->get_logger(),"s = %.2f , ns = %ld",right_now_1.seconds(),right_now_1.nanoseconds());
RCLCPP_INFO(this->get_logger(),"s = %.2f , ns = %ld",right_now_2.seconds(),right_now_2.nanoseconds());
}
};
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}



和Time類似
但是不完全相同,這個duration用到了chrono。






t2和t1可以進行相減,結果是一個duration型別的,但是不能相加。
time也可以和duration相加相減,結果是一個time。






8.通訊機制工具
8.1.命令列



ros2 doctor是來檢測系統網路狀態、版本相容性等狀態的。


是通過了的,但是有幾個警告:版本過低,不影響正常使用。





引數服務端的本質還是服務端,所以也會列在Service Servers裡。


如果檢視list發現有一些介面檔案沒顯示,那說明你的這個終端的環境變數沒重新整理。


proto比show更精簡一些。


pose是傳送位姿的,會一直髮資料。



想輸出延時,訊息必須有訊息頭。

輸出即時位姿


find和type是相反著來的。

訊息釋出頻率是不斷變動的,訊息頻率是可以通過定時器來控制,但是定時器是有誤差的,並不是特別特別精準。當然還有網路也是一大影響因素。

ros2 topic pub -r 釋出訊息的頻率 話題名稱 訊息 具體的指令(要用json格式)






clear是清除烏龜軌跡
kill是殺烏龜
reset是將烏龜位置重置
spawn是產卵,生成新烏龜

可以按Tab補齊


type和find是相反的

empty就是空,所以我們後面內容啥都不用寫。






可以加上-f或者-feedback開啟連續反饋,這個連續反饋是航向角弧度







刪除不是所有引數都能刪除,這裡提示不能刪除靜態型別引數。

最大值255,最小取值0
步長1

可以顯示在終端上,


也可以寫入磁碟


當然也可以修改





也可以用ROS2 RUN來修改,--ros-args -p 後面跟 鍵:=值



ROS2 RUN也可以直接讀取磁碟檔案

8.2.Rqt工具箱









這個是顯示兩個節點之間關係的。


這個是用來發布訊息的。

點選新增按鈕

可以設定線速度和角速度,頻率等

設定好引數後勾選




call /clear這個是清除軌跡的。

也可以產卵,設定好引數



這個可以直接修改引數

我們不能用rqt代替命令列,雖然rqt更方便,但是因為我們在工作中是遠端控制機器人的,我們是通過terminal來遠端控制機器人,所以,命令列很重要,這樣不能使用rqt。