--- tags : DIT 2023寒假 -- ROS教學 --- # DAY 3 -- node間的通訊架構 {%hackmd BJrTq20hE %} ## <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 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 ``` 執行後會開啟一個視窗,如下圖: ![](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 3 的內容比較繁多,但都是ROS最基本常用的功能,熟練後基本上已經可以應付大部分簡單的需求了~~~~ 看到這裡的你們好棒!辛苦了!