# 6.3 topic in roscpp
## 6.3.1 Topic通信
Topic是ROS里一種異步通信的模型,一般是節點間分工明確,有的只負責發送,有的只負責接收處理。對于絕大多數的機器人應用場景,比如傳感器數據收發,速度控制指令的收發,Topic模型是最適合的通信方式。
為了講明白topic通信的編程思路,我們首先來看`topic_demo`中的代碼,這個程序是一個消息收發的例子:**自定義一個類型為gps的消息(包括位置x,y和工作狀態state信息),一個node以一定頻率發布模擬的gps消息,另一個node接收并處理,算出到原點的距離。**
源代碼見`ROS-Academy-for-Beginners/topic_demo`
## 6.3.2 創建gps消息
在代碼中,我們會用到自定義類型的gps消息,因此就需要來自定義gps消息,在msg路徑下創建`gps.msg`:
見`topic_demo/msg/gps.msg`
```
string state #工作狀態
float32 x #x坐標
float32 y #y坐標
```
以上就定義了一個gps類型的消息,你可以把它理解成一個C語言中的結構體,類似于
```cpp
struct gps
{
string state;
float32 x;
float32 y;
}
```
在程序中對一個gps消息進行創建修改的方法和對結構體的操作一樣。
當你創建完了msg文件,記得修改`CMakeLists.txt`和`package.xml`,從而讓系統能夠編譯自定義消息。
在`CMakeLists.txt`中需要改動
```cmake
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_generation #需要添加的地方
)
add_message_files(FILES gps.msg)
#catkin在cmake之上新增的命令,指定從哪個消息文件生成
generate_messages(DEPENDENCIES std_msgs)
#catkin新增的命令,用于生成消息
#DEPENDENCIES后面指定生成msg需要依賴其他什么消息,由于gps.msg用到了flaot32這種ROS標準消息,因此需要再把std_msgs作為依賴
```
`package.xml`中需要的改動
```xml
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
```
當你完成了以上所有工作,就可以回到工作空間,然后編譯了。編譯完成之后會在`devel`路徑下生成`gps.msg`對應的頭文件,頭文件按照C++的語法規則定義了`topic_demo::gps`類型的數據。
要在代碼中使用自定義消息類型,只要`#include <topic_demo/gps.h>`,然后聲明,按照對結構體操作的方式修改內容即可。
```cpp
topic_demo::gps mygpsmsg;
mygpsmsg.x = 1.6;
mygpsmsg.y = 5.5;
mygpsmsg.state = "working";
```
## 6.3.3 消息發布節點
定義完了消息,就可以開始寫ROS代碼了。通常我們會把消息收發的兩端分成兩個節點來寫,一個節點就是一個完整的C++程序。
見`topic_demo/src/talker.cpp`
```cpp
#include <ros/ros.h>
#include <topic_demo/gps.h> //自定義msg產生的頭文件
int main(int argc, char **argv)
{
ros::init(argc, argv, "talker"); //用于解析ROS參數,第三個參數為本節點名
ros::NodeHandle nh; //實例化句柄,初始化node
topic_demo::gps msg; //自定義gps消息并初始化
...
ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_info", 1); //創建publisher,往"gps_info"話題上發布消息
ros::Rate loop_rate(1.0); //定義發布的頻率,1HZ
while (ros::ok()) //循環發布msg
{
... //處理msg
pub.publish(msg);//以1Hz的頻率發布msg
loop_rate.sleep();//根據前面的定義的loop_rate,設置1s的暫停
}
return 0;
}
```
機器人上幾乎所有的傳感器,幾乎都是按照固定頻率發布消息這種通信方式來傳輸數據,只是發布頻率和數據類型的區別。
## 6.3.4 消息接收節點
見`topic_demo/src/listener.cpp`
```cpp
#include <ros/ros.h>
#include <topic_demo/gps.h>
#include <std_msgs/Float32.h>
void gpsCallback(const topic_demo::gps::ConstPtr &msg)
{
std_msgs::Float32 distance; //計算離原點(0,0)的距離
distance.data = sqrt(pow(msg->x,2)+pow(msg->y,2));
ROS_INFO("Listener: Distance to origin = %f, state: %s",distance.data,msg->state.c_str()); //輸出
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("gps_info", 1, gpsCallback); //設置回調函數gpsCallback
ros::spin(); //ros::spin()用于調用所有可觸發的回調函數,將進入循環,不會返回,類似于在循環里反復調用spinOnce()
//而ros::spinOnce()只會去觸發一次
return 0;
}
```
在topic接收方,有一個比較重要的概念,就是**回調(CallBack)**,在本例中,回調就是預先給`gps_info`話題傳來的消息準備一個回調函數,你事先定義好回調函數的操作,本例中是計算到原點的距離。只有當有消息來時,回調函數才會被觸發執行。具體去觸發的命令就是`ros::spin()`,它會反復的查看有沒有消息來,如果有就會讓回調函數去處理。
因此千萬不要認為,只要指定了回調函數,系統就回去自動觸發,你必須`ros::spin()`或者`ros::spinOnce()`才能真正使回調函數生效。
## 6.3.5 CMakeLists.txt文件修改
在`CMakeLists.txt`添加以下內容,生成可執行文件
```cmake
add_executable(talker src/talker.cpp) #生成可執行文件talker
add_dependencies(talker topic_demo_generate_messages_cpp)
#表明在編譯talker前,必須先生編譯完成自定義消息
#必須添加add_dependencies,否則找不到自定義的msg產生的頭文件
#表明在編譯talker前,必須先生編譯完成自定義消息
target_link_libraries(talker ${catkin_LIBRARIES}) #鏈接
add_executable(listener src/listener.cpp ) #聲稱可執行文件listener
add_dependencies(listener topic_demo_generate_messages_cpp)
target_link_libraries(listener ${catkin_LIBRARIES})#鏈接
```
以上cmake語句告訴catkin編譯系統如何去編譯生成我們的程序。這些命令都是標準的cmake命令,如果不理解,請查閱cmake教程。
之后經過`catkin_make`,一個自定義消息+發布接收的基本模型就完成了。
## 擴展:回調函數與spin()方法
回調函數在編程中是一種重要的方法,在維基百科上的解釋是:
```
In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time.
```
回調函數作為參數被傳入到了另一個函數中(在本例中傳遞的是函數指針),在未來某個時刻(當有新的message到達),就會立即執行。Subscriber接收到消息,實際上是先把消息放到一個**隊列**中去,如圖所示。隊列的長度在Subscriber構建的時候設置好了。當有spin函數執行,就會去處理消息隊列中隊首的消息。

spin具體處理的方法又可分為阻塞/非阻塞,單線程/多線程,在ROS函數接口層面我們有4種spin的方式:
| spin方法 | 阻塞 | 線程 |
| :------: | :------: | :------: |
| `ros::spin()` | 阻塞 | 單線程|
| `ros::spinOnce()` |非阻塞 | 單線程 |
| `ros::MultiThreadedSpin()` | 阻塞 | 多線程 |
| `ros::AsyncMultiThreadedSpin()` | 非阻塞 |多線程 |
阻塞與非阻塞的區別我們已經講了,下面來看看單線程與多線程的區別:

我們常用的`spin()`、`spinOnce()`是單個線程逐個處理回調隊列里的數據。有些場合需要用到多線程分別處理,則可以用到`MultiThreadedSpin()`、`AsyncMultiThreadedSpin()`。
- 前言
- 第一章 ROS簡介
- 機器人時代的到來
- ROS發展歷程
- 什么是ROS
- 安裝ROS
- 安裝ROS-Academy-for-Beginners教學包
- 二進制與源碼包
- 安裝RoboWare Studio
- 單元測試一
- 第二章 ROS文件系統
- Catkin編譯系統
- Catkin工作空間
- Package軟件包
- CMakeLists.txt
- package.xml
- Metapacakge軟件元包
- 其他常見文件類型
- 單元測試二
- 第三章 ROS通信架構(一)
- Node & Master
- Launch文件
- Topic
- Msg
- 常見msg類型
- 單元測試三
- 第四章 ROS通信架構(二)
- Service
- Srv
- Parameter server
- Action
- 常見srv類型
- 常見action類型
- 單元測試四
- 第五章 常用工具
- Gazebo
- RViz
- Rqt
- Rosbag
- Rosbridge
- moveit!
- 單元測試五
- 第六章 roscpp
- Client Library與roscpp
- 節點初始、關閉與NodeHandle
- Topic in roscpp
- Service in roscpp
- Param in roscpp
- 時鐘
- 日志與異常
- 第七章 rospy
- Rospy與主要接口
- Topic in rospy
- Service in rospy
- Param與Time
- 第八章 TF與URDF
- 認識TF
- TF消息
- tf in c++
- tf in python
- 統一機器人描述格式
- 附錄:TF數學基礎
- 三維空間剛體運動---旋轉矩陣
- 三維空間剛體運動---歐拉角
- 三維空間剛體運動---四元數
- 第九章 SLAM
- 地圖
- Gmapping
- Karto
- Hector
- 第十章 Navigation
- Navigation Stack
- move_base
- costmap
- Map_server & Amcl
- 附錄:Navigation工具包說明
- amcl
- local_base_planner
- carrot_planner
- clear_costmap_recovery
- costmap_2d
- dwa_local_planner
- fake_localization
- global_planner
- map_server
- move_base_msg
- move_base
- move_slow_and_clear
- navfn
- nav_core
- robot_pose_ekf
- rotate_recovery