---
# System prepended metadata

title: DAY 2 -- node間的通訊架構
tags: [DIT 12th 教學 -- ROS1]

---

---
tags : DIT 12th 教學 -- ROS
---


# DAY 2 -- node間的通訊架構
{%hackmd @HungPin/BkVDWAea3 %}

## <font color="orange"> 01. ROS topic介紹</font>
在ROS之中，node之間交換資料的主要方式是發送以及接收messages，而messages會在topic上傳輸。

以下是topic的架構圖：

![](https://i.imgur.com/RASKJCx.png)


- node1想要分享message，將message **publish**發步到topic上
- node2想要接收message，則是使用**subscribe**從topic上拿到message！

:::danger
**特別要注意的地方：**
* 一個topic可以有"**複數個**"Publisher與Subscriber
* 同一個topic之中，只能使用同一種message型態
:::
 <font color ="yellow"> 以下是常用的topic指令： </font>
 

```=
rostopic list
rostopic list -v
```
列出當前master中有在運作的topic name
加上`-v`會列出更詳細的資料
```=
rostopic echo [topic_name]
```
在`[topic_name]`的位置填入topic的名稱，監控該topic發布的message內容，會在終端裡印出來
```=
rostopic info [topic_name]
```
在`[topic_name]`的位置填入topic的名稱
印出topic的message type、列出有哪些node publish、subscribe這個topic

示範：

```=
~$ rostopic info rosout
Type: rosgraph_msgs/Log

Publishers: None

Subscribers: 
 * /rosout 
```

**可以試試看在terminal輸入rostopic，列出所有rostopic的功能！**

## <font color="orange"> 02. 常用的messages</font>
ROS原先就有提供相當多種message格式，基本上滿足大部分的使用需求

而message由下列幾種資料型態構成：

![](https://i.imgur.com/xgFAiHU.png)

<font color ="yellow"> 以下介紹幾種常見的message格式 </font>

* **std_msgs**
    全名叫standard message，是最常用到的package。
    在這個package底下有許多種message，例如：`Int32、Int64、        Float64、String`...
    
    可以點這個[連結](http://docs.ros.org/en/noetic/api/std_msgs/html/index-msg.html)看看全部的格式:
    
* **geometry_msgs**
    主要多用在記錄位置、姿態等資料。
    常用的有：
   **1. geometry_msgs/Point.msg**
    > ```
    >float64 x
    >float64 y
    >float64 z
    > ```
    > 常用來儲存(x,y,z)位置的資訊。[color=#3]
    > :::info
    > **例子：**
    > 如果想讓車子走到一個指定的座標，就可以利用這個通訊格式。
    > ```cpp=
    > //宣告一個使用此格式的變數，並存入(10,0,0)的座標。
    > geometry_msgs::Point point; 
    > point.x = 10;
    > point.y = 0;
    > point.z = 0;
    > ```
    > :::

    **2. geometry_msgs/Twist.msg** 
    >```
    >geometry_msgs/Vector3 linear
    >geometry_msgs/Vector3 angular
    >```
    > 可以儲存速度以及角速度的資訊。[color=#3]
    > :::info
    > **例子：**
    > 讓車子以 1 的速度向前走，並同時以1的角速度逆時針旋轉。
    > ```cpp=
    > geometry_msgs::Twist vel; 
    > vel.linear.x = 1;
    > vel.linear.y = 0;
    > vel.linear.z = 0;
    > vel.angular.x = 0;
    > vel.angular.y = 0;
    > vel.angular.z = 1;
    > ```
    > :warning: **ROS 普遍使用的是 [ROS_REP 103](https://www.ros.org/reps/rep-0103.html)坐標系規定，也就是x指向前方，y指向左方，z指向上方，至於旋轉則是以右手定則表示為正。**
    > 
    > **有興趣的可以看看[這個網站](https://zhuanlan.zhihu.com/p/140334339)**
    > :::
    
    **3. geometry_msgs/Quaternion.msg**
    >```
    >float64 x
    >float64 y
    >float64 z
    >float64 w
    >```
    > 記錄[四元數](https://zh.wikipedia.org/zh-tw/%E5%9B%9B%E5%85%83%E6%95%B8)，也就是將三維空間中的旋轉，以四維的複數空間來表示，這部分先不會用到，有興趣可以自己再查看看。比較常被拿來記錄紀錄機器人的面向。[color=#3]
    
 * `nav_msgs`、`Imu.msg`、`LaserScan.msg` 
 還有許多好用的message，但在這裡就不多作介紹了，如果想看更多可以直接搜尋`message名稱 + Documentation`就可以找到該通訊格式的詳細資料型態。
 :::info
 除了ROS定義好的messages以外，還可以自己定義message，在之後會做介紹！
 :::
 
    
    

## <font color="orange"> 03. Messages 的使用方式</font>

接下來介紹在node之中如何使用message，這裡我們先用`std_msgs/Int64.msg`做示範。

<font color ="yellow">Step 1. 建立一個 node。</font>

```=
cd winter_2023/src/practice/src
code talker.cpp
```

打開terminal，移動到package底下的src資料夾，創建一個叫做`talker`的cpp檔。

<font color ="yellow">Step 2. 將測試程式直接複製貼上。</font>

```cpp=
#include "ros/ros.h"
#include "std_msgs/Int64.h"

int main(int argc, char **argv)
{
    ros::init(argc, argv, "talker");
    ros::NodeHandle nh;
    ros::Rate loop_rate(1);
    
    int number = 0;
    std_msgs::Int64 msg;
    
    while(ros::ok())
    {
        msg.data = number;
        ROS_INFO("%d", msg.data);
        number++;
        loop_rate.sleep();
    }
    return 0;
}
```

<font color ="yellow">Step 3. 存檔後修改 CMakeLists.txt。</font> 

```cpp=
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
```
:::success
**需要注意的是在之前創建package時打的那一行：**
`catkin_create_pkg [package_name] roscpp rospy std_msgs`

>在`[package_name]`後面的後綴`roscpp`、`rospy`、`std_msgs`，會自動生成在CMakeLists.txt內(大概第10行)

```cpp=
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
)
```
**如果後續package中有使用到新的message，可以參考下列參考資料：**
:::spoiler
>**1. 記得要在find_package中加入新的message名稱！**
>
>**2. package.xml也要做修改**
>* 在第51行`<buildtool_depend>catkin</buildtool_depend>`    的後面可以看到：
```txt=
//build_depend 編譯pkg前須先編譯的其他pkg
<build_depend>roscpp</build_depend
<build_depend>rospy</build_depend>
<build_depend>std_msgs</build_depend>
//build_export_depend 編譯pkg時include的lib名稱
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>rospy</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
//exec_depend 編譯pkg時須執行的其它pkg
<exec_depend>roscpp</exec_depend>
<exec_depend>rospy</exec_depend>
<exec_depend>std_msgs</exec_depend>
```
>照這個格式加入新的message！
>也可以用`<depend>`直接合併上面三個tag
```txt=
<depend> rospy </depend>
<depend> roscpp </depend>
<depend> std_msgs </depend>
```
:::
<font color ="yellow">Step 4. 回到 workspace 資料夾編譯 & source。</font> 
```=
cd ~
cd winter2023
catkin_make
source devel/setup.bash
```
<font color ="yellow">Step 5. 按下 ctrl + alt + T 開新視窗，並在新視窗開啟 ros master。</font>

```=
roscore
```
<font color ="yellow">Step 6. 回到原本視窗，執行 node。</font>

```=
rosrun practice talker
```
<font color ="yellow">Step 7. code 解析。</font>
接下來我們來一步一步看這份code吧！
```cpp=
#include "ros/ros.h"
#include "std_msgs/Int64.h"

int main(int argc, char **argv)
{
    ros::init(argc, argv, "talker");
    ros::NodeHandle nh;
    ros::Rate loop_rate(1);
    
    int number = 0;
    std_msgs::Int64 msg;
    
    while(ros::ok())
    {
        msg.data = number;
        ROS_INFO("%ld", msg.data);
        number++;
        loop_rate.sleep();
    }
    return 0;
}
```
* 引入message的header file
>`#include "std_msgs/Int64.h"`
將std_msgs package底下的Int64.msg引入到node中

* 宣告message變數
>`std_msgs::Int64 msg;`
>宣告一個Int64資料型態的standard message 叫做msg

* message賦予值
>`msg.data = number;`
>將一個叫number的變數寫入msg message中的data欄位 

:bookmark_tabs: **這裡有用到[DAY 1](/s/jDY2Q1grRSuzXbHND4WVdA?view#-04-ROS-程式架構初探)教的內容，如果忘記的話可以再回去看一次喔！**

## <font color="orange"> 04. ROS Publisher & Subscriber </font>

- ### <font color="yellow">Publisher - C++</font>

<font color ="yellow">Step 1. 建立Publisher。</font>

將先前創立的`talker`修改成這樣：

```cpp=
#include "ros/ros.h"
#include "std_msgs/Int64.h"

int main(int argc, char **argv)
{
    ros::init(argc, argv, "talker");
    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::Int64>("counter", 10);
    ros::Rate loop_rate(1);
    
    int number = 0;
    std_msgs::Int64 msg;
    
    while(ros::ok())
    {
        msg.data = number;
        // ROS_INFO("%ld", msg.data);
        pub.publish(msg);
        
        number++;
        loop_rate.sleep();
    }
    return 0;
}
```
<font color ="yellow">Step 2. code 解析。</font>

```cpp=
ros::Publisher pub = nh.advertise<std_msgs::Int64>("counter", 10);
```
* `advertise()`會將建立的topic資料告訴master node，回傳一個Publisher物件(這裡為pub)。
* `std_msgs::Int64`為std_msgs package 中的Int64.msg，可自行選擇要使用的message。
* `"counter"`為topic name，可以自行設定。
* `10`是指message queue size，當publish數量太多，超過10筆message時，比較舊的message就會被捨棄，並由最新的message替代填入topic。

```cpp=
pub.publish(msg);
```
使用前面建立的Publisher物件pub的publish()方法，將暫存的msg送出 (publish)
<font color ="yellow">Step 3. 編譯 & 執行。</font>

* 回到workspace下使用catkin_make編譯
* source devel/setup.bash
* rosrun practice talker
* <font color ="orange">**記得要roscore**</font>

:::info
編譯的動作會很常用到，要多多練習熟練這些步驟！

編譯執行後可以使用`rostopic list` 、`rostopic echo`等功能查看topic有沒有存在！
:::


---

- ### <font color="yellow">Subscriber - C++</font>

<font color ="yellow">Step 1. 建立 Subscriber。</font>

```=
cd winter_2023/src/practice/src
code listener.cpp
```
listener.cpp
```cpp=
#include "ros/ros.h"
#include "std_msgs/Int64.h"

void Callback(const std_msgs::Int64::ConstPtr& msg)
{
    std::cout<<"msg.data = "<<msg->data<<"\n";
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("counter",10,Callback);
    ros::Rate loop_rate(1);
    
    while(ros::ok())
    {
        ros::spinOnce();
        loop_rate.sleep();
    }
    return 0;
}
```
<font color ="yellow">Step 2. code 解析。</font>


```cpp=
void Callback(const std_msgs::Int64::ConstPtr& msg)
```
subscriber的回調函式，參數為所接收的message的**常數指標**。


```cpp=
ros::Subscriber sub = nh.subscribe("counter",10,Callback);
```
* `subscribe()`會將建立topic的資訊告訴master node，回傳一個Subscriber物件(在此為sub)。
* `"counter"`為topic name，可以自行設定。
* `10`指的是message queue，若接收得太快，超過10個message，舊的message會被捨棄。
* 指定的回呼函式名稱為`Callback`。

```cpp=
ros::spinOnce();
```
ROS消息回調函式，當程式執行到這行時，會去調用callback function(在這裡叫做Callback)，這時才可以接收topic上面的message！

:ghost: 
也有另一種ROS消息回調函式`ros::spin()`，當程式執行到這行，`ros::spin()`在調用後不會再返回，也就是程式執行到這就不會執行下去了！會待在callback function內**重複調用**！


---


- ### <font color="yellow">Publisher - Python</font>
這部分是將C++的node轉換成Python的寫法
```python=
#!/usr/bin/env python3 
#coding:utf-8

import rospy
from std_msgs.msg import Int64

def publisher():
    rospy.init_node('talker')
    pub = rospy.Publisher('counter', Int64, queue_size = 10)
    count = 0
    while not rospy.is_shutdown():
            pub.publish(count)
            rospy.sleep(1)
            count+=1
    
if __name__=='__main__':
    try:
        publisher()
    except rospy.ROSInterruptException:
        pass
```
* `publisher_name =rospy.Publisher (“topic_name”,  msg type,  queue_size )`

相信大家應該都發現Python跟C++在寫法上都十分的相似吧！只是語法上有些差異而已，可以自己練習，選擇自己喜歡的語言去撰寫！

---
- ### <font color="yellow">Subscriber - Python</font>
```python=
#!/usr/bin/env python3
#coding:utf-8

import rospy
from std_msgs.msg import Int64

def callbackfunc(msg):
    rospy.loginfo("%d\n",msg.data)

def subscriber():
    rospy.init_node('listener')
    rospy.Subscriber('counter', Int64, callbackfunc)
    rospy.spin()
    
if __name__=='__main__':
    try:
        subscriber()
    except rospy.ROSInterruptException:
        pass
```
* `rospy.Subscriber('topic_name', msg type, callback function)`

## <font color="orange"> 05. turtlesim</font>

### <font color="yellow"> turtlesim 介紹</font>
在介紹過Publisher以及Subscriber後，我們可以使用ROS內建的小烏龜來玩玩看實際應用的方式囉！
* 安裝小烏龜(通常已經和ROS一起安裝好)
```txt=
sudo apt-get install ros-noetic-turtlesim
```

* 打開小烏龜
```txt=
rosrun turtlesim turtlesim_node
```

執行後會開啟一個視窗，如下圖：

![](https://i.imgur.com/mAFrs9h.png)


* 使用鍵盤鍵上下左右控制小烏龜
```txt=
rosrun turtlesim turtle_teleop_key
```

* 讓小烏龜自動走正方形
```txt=
rosrun turtlesim draw_square
```
可以使用topic list列出目前運行的topic。
```txt=
~$ rostopic list
/rosout
/rosout_agg
/turtle1/cmd_vel //Type: geometry_msgs/Twist 控制小烏龜移動
/turtle1/color_sensor//Type: turtlesim/Color 控制小烏龜的顏色
/turtle1/pose//Type: turtlesim/Pose 小烏龜的位置
```
* 有關於/turtle1的topic是在執行`turtlesim_node`時生成的！

---
### <font color="yellow">turtlesim 練習</font>

 **提示都在詳細資料內，可以自己先試過在查看！**
:::success
**Step 1. 建立新的package。**

>請先建立一個新的package，dependencies必須包含
`roscpp、rospy、std_msgs、geometry_msgs、turtlesim`

:::spoiler
```txt=
catkin_create_pkg package_name roscpp rospy std_msgs geometry_msgs turtlesim
```
> 如果沒在一開始就加入dependencies，也可以從CmakeList、Package.xml上修改。
:::


:::success
**Step 2. 建立一個node，同時具有publish和subscribe的功能。**

>須包含一個Publisher發布`geometry_msgs::Twist`到`/turtle1/cmd_vel`上
以及一個Subscriber從`/turtle1/pose`上接收`turtlesim::Pose`。

:::spoiler
```cpp=
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Pose.h>

void callback(const turtlesim::Pose::ConstPtr& pose_data)
{
...
}

int main(int argc, char **argv)
{
    ros::init(argc,argv,"move_control");
    ros::NodeHandle nh;
    ros::Publisher vel_pub = nh.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel",1);
    ros::Subscriber pose_sub = nh.subscribe("/turtle1/pose",10,callback);
    ros::Rate rate(10);
    geometry_msgs::Twist vel_msg;
    ...
}
```
:::

:::success
**Step 3. 控制小烏龜向前走到 (10,0)。**

而既然可以取得小烏龜的位置和控制小烏龜的速度，我們就可以控制小烏龜走特定的距離。
>一般小烏龜生成時所在的座標點為
>`x=[5.544445]、y=[5.544445]、theta=[0.000000]`
>給小烏龜一個向前 (x) 的速度，當 接收到 `/turtle1/pose` 的位置超過時，立即停止小烏龜 。

:::spoiler
> 利用`/turtle1/cmd_vel`這個 topic 控制小烏龜的速度。
> 接收 `/turtle1/pose` 這個 topic 已得知小烏龜的位置。
> 如用上述的做法， 小烏龜可能會有較大的誤差，可以想想要怎麼讓誤差盡可能的小 ！
:::


:+1: DAY 2 的內容比較繁多，但都是ROS最基本常用的功能，熟練後基本上已經可以應付大部分簡單的需求了~~~~
看到這裡的你們好棒！辛苦了！
