第 5 節

ROS2其他通信機制

0瀏覽次數0訪問次數--跳出率--平均停留

概述


不同設備之間的通信,是通過分佈式來實現的。


鏡像王八

分佈式搭建

場景

在許多機器人相關的應用場景中都涉及到多台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的值時並不是隨意的,也是有一定約束的:

  1. 建議ROS_DOMAIN_ID的取值在[0,101] 之間,包含0和101;
  2. 每個域ID內的節點總數是有限制的,需要小於等於120個;
  3. 如果域ID為101,那麼該域的節點總數需要小於等於54個。

DDS 域 ID 值的計算規則

域ID值的相關計算規則如下:

  1. DDS是基於TCP/IP或UDP/IP網絡通信協議的,網絡通信時需要指定端口號,端口號由2個字節的無符號整數表示,其取值範圍在[0,65535]之間;
  2. 端口號的分配也是有其規則的,並非可以任意使用的,根據DDS協議規定以7400作為起始端口,也即可用端口為[7400,65535],又已知按照DDS協議默認情況下,每個域ID佔用250個端口,那麼域ID的個數為:(65535-7400)/250 = 232(個),對應的其取值範圍為[0,231];
  3. 操作系統還會設置一些預留端口,在DDS中使用端口時,還需要避開這些預留端口,以免使用中產生衝突,不同的操作系統預留端口又有所差異,其最終結果是,在Linux下,可用的域ID為[0,101]與[215-231],在Windows和Mac中可用的域ID為[0,166],綜上,為了兼容多平台,建議域ID在[0,101] 範圍內取值。
  4. 每個域ID默認佔用250個端口,且每個ROS2節點需要佔用兩個端口,另外,按照DDS協議每個域ID的端口段內,第1、2個端口是Discovery Multicast端口與User Multicast端口,從第11、12個端口開始是域內第一個節點的Discovery Unicast端口與User Unicast,後續節點所佔用端口依次順延,那麼一個域ID中的最大節點個數為:(250-10)/2 = 120(個);
  5. 特殊情況:域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,並打開烏龜顯示節點。

會發現是可以正常通信的。

工作空間覆蓋

沒什麼用,建議不要使用

這個是和你的bashrc文件裏,加載bash文件的順序有關,誰最後加載,誰就會運行,也就是最高優先級。除了ROS2本身自帶的bash,這個bash是不論在哪加載,都是最低優先級。


元功能包

distro就是發行版的意思


--build-type默認C++,

--dependent默認無,

--node-name本身是虛包,所以也無需設置。

CmakeLists無需修改

先把12,13行刪除

<exec_depend>xxxxxx</exec_depend>

所要依賴的功能包名


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

這個功能包是導航相關的

這個就是個元功能包

只有配置文件,沒有其他實質性實現

元功能包的作用就是方便安裝,把自己的東西打包,可以共享到ROS2社區。也方便安裝別人的東西。

節點重名

而且節點名稱都是一致的,圖中有個<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;
}

話題重名


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;
}

全局話題

相對話題

私有話題

時間相關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。

通信機制工具

命令行


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也可以直接讀取磁盤文件

Rqt工具箱


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


這個是用來發布消息的。

點擊添加按鈕

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

設置好參數後勾選


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

也可以產卵,設置好參數


這個可以直接修改參數


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

音乐页