---
tags : DIT 2023寒假 -- ROS教學
---
# DAY 3 -- node間的通訊架構
{%hackmd BJrTq20hE %}
## <font color="orange"> 01. ROS topic介紹</font>
在ROS之中,node之間交換資料的主要方式是發送以及接收messages,而messages會在topic上傳輸。
以下是topic的架構圖:

- 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由下列幾種資料型態構成:

<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 2](/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
```
執行後會開啟一個視窗,如下圖:

* 使用鍵盤鍵上下左右控制小烏龜
```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 3 的內容比較繁多,但都是ROS最基本常用的功能,熟練後基本上已經可以應付大部分簡單的需求了~~~~
看到這裡的你們好棒!辛苦了!