第二章、ROS通信机制

机器人是一种高度复杂的系统性实现,在机器人上可能集成各种传感器(雷达、摄像头、GPS…)以及运动控制实现,为了解耦合,在ROS中每一个功能点都是一个单独的进程,每一个进程都是独立运行的。更确切的讲,ROS是进程(也称为Nodes)的分布式框架。 因为这些进程甚至还可分布于不同主机,不同主机协同工作,从而分散计算压力。不过随之也有一个问题: 不同的进程是如何通信的?也即不同进程间如何实现数据交换的?在此我们就需要介绍一下ROS中的通信机制了。 ROS 中的基本通信机制主要有如下三种实现策略:
  • 话题通信(发布订阅模式)
  • 服务通信(请求响应模式)
  • 参数服务器(参数共享模式)
本章的主要内容就是是介绍各个通信机制的应用场景理论模型代码实现以及相关操作命令。本章预期达成学习目标如下:
  • 能够熟练介绍ROS中常用的通信机制
  • 能够理解ROS中每种通信机制的理论模型
  • 能够以代码的方式实现各种通信机制对应的案例
  • 能够熟练使用ROS中的一些操作命令
  • 能够独立完成相关实操案例
案例演示: 案例演示:

1.话题演示案例:

控制小乌龟做圆周运动 获取乌龟位姿

2.服务演示案例:在指定位置生成乌龟

**3.参数演示案例:改变乌龟窗口的背景颜色**

1、话题通讯(已验证)

话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如下面一个常见场景:

机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。

在上述场景中,就不止一次使用到了话题通信。

以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要时时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。

再以运动消息的发布为例,导航模块会根据传感器采集的数据时时的计算出运动控制信息并发布给底盘,底盘也可以有一个节点订阅运动信息并最终转换成控制电机的脉冲信号。

以此类推,像雷达、摄像头、GPS…. 等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。

概念

以发布订阅的方式实现不同节点之间数据交互的通信模式。

作用

用于不断更新的、少逻辑处理的数据传输场景。

案例(下面讲述)

1.实现最基本的发布订阅模型,发布方以固定频率发送一段文本,订阅方接收文本并输出。(2.1.2 — 2.1.3)

2.实现对自定义消息的发布与订阅。(2.1.4 — 2.1.6)

1、话题通信理论模型

话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:
  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。

第二章、ROS通信机制 - 图1

整个流程由以下步骤实现:

0.Talker注册

Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。

1.Listener注册

Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。

2.ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。

3.Listener向Talker发送请求

Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。

4.Talker确认请求

Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。

5.Listener与Talker件里连接

Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。

6.Talker向Listener发送消息

连接建立后,Talker 开始向 Listener 发布消息。 注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议 注意2: Talker 与 Listener 的启动无先后顺序要求 注意3: Talker 与 Listener 都可以有多个 注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。

2.1、话题通信基本操作A(C++)

需求:

编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
  1. 1. **<font style="color:rgb(51, 51, 51);">发布方</font>**
  2. 2. **<font style="color:rgb(51, 51, 51);">接收方</font>**
  3. 3. **<font style="color:rgb(51, 51, 51);">数据(此处为自定义消息</font>**<font style="color:rgb(51, 51, 51);">)</font>

流程:

  1. 1. <font style="color:rgb(51, 51, 51);">编写发布方实现;</font>
  2. 2. <font style="color:rgb(51, 51, 51);">编写订阅方实现;</font>
  3. 3. <font style="color:rgb(51, 51, 51);">编辑配置文件;</font>
  4. 4. <font style="color:rgb(51, 51, 51);">编译并执行。</font>

1.发布方

  1. /*
  2. 需求: 实现基本的话题通信,一方发布数据,一方接收数据,
  3. 实现的关键点:
  4. 1.发送方
  5. 2.接收方
  6. 3.数据(此处为普通文本)
  7. PS: 二者需要设置相同的话题
  8. 消息发布方:
  9. 循环发布信息:HelloWorld 后缀数字编号
  10. 实现流程:
  11. 1.包含头文件
  12. ros中的文本类型---<std_msgs/String.h>
  13. 2.初始化 ROS 节点:命名(唯一)
  14. 3.实例化 ROS 句柄
  15. 4.实例化 发布者 对象
  16. 5.组织被发布的数据,并编写逻辑发布数据
  17. */
  18. // 1.包含头文件
  19. #include "ros/ros.h"
  20. #include "std_msgs/String.h" //普通文本类型的消息
  21. #include <sstream>
  22. int main(int argc, char *argv[])
  23. {
  24. //设置编码
  25. setlocale(LC_ALL,"");
  26. //2.初始化 ROS 节点:命名(唯一)
  27. // 参数1和参数2 后期为节点传值会使用
  28. // 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
  29. ros::init(argc,argv,"talker");
  30. //3.实例化 ROS 句柄
  31. ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能
  32. //4.实例化 发布者 对象
  33. //泛型: 发布的消息类型std_msgs::String
  34. //参数1: 要发布到的话题"chatter"
  35. //参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
  36. ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
  37. //5.组织被发布的数据,并编写逻辑发布数据
  38. //数据(动态组织)
  39. std_msgs::String msg;
  40. // msg.data = "你好啊!!!";
  41. std::string msg_front = "Hello 你好!"; //消息前缀
  42. int count = 0; //消息计数器
  43. //逻辑(一秒10次)
  44. ros::Rate r(1); //注2
  45. //节点不死
  46. while (ros::ok()) //ok
  47. {
  48. //使用 stringstream 拼接字符串与编号
  49. std::stringstream ss; //实例化
  50. ss << msg_front << count; //将msg_frontcount进行拼接
  51. msg.data = ss.str();
  52. //发布消息
  53. pub.publish(msg);
  54. //加入调试,打印发送的消息
  55. ROS_INFO("发送的消息:%s",msg.data.c_str()); //见注1
  56. //根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
  57. r.sleep();
  58. count++;//循环结束前,让 count 自增
  59. //暂无应用
  60. ros::spinOnce(); //注3
  61. }
  62. return 0;
  63. }

注:

(1)c_str() 以 char 形式传回 string 内含字符串
如果一个函数要求char
参数,可以使用c_str()方法:

c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同.
这是为了与c语言兼容,在c语言中没有string类型故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。

1.c_str是一个内容为字符串指向字符数组的临时指针;
**
2.c_str返回的是一个可读不可改的常指针;**

  1. string s = "Hello World!";
  2. printf("%s", s.c_str()); //输出 "Hello World!"

(2)ros::Rate xx 是一个定时器函数。单位是hz。通过xx.sleep()休眠来度过剩下的时间。

(3)ros::spin()与ros::spinOnce() 两者都叫**消息回调处理函数。两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回到函数的原理**

如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回到函数的原理.

(4)rostopic list

rostopic echo 话题名称 (在屏幕上打印该话题消息)

2.订阅方

  1. /*
  2. 需求: 实现基本的话题通信,一方发布数据,一方接收数据,
  3. 实现的关键点:
  4. 1.发送方
  5. 2.接收方
  6. 3.数据(此处为普通文本)
  7. 消息订阅方:
  8. 订阅话题并打印接收到的消息
  9. 实现流程:
  10. 1.包含头文件
  11. 2.初始化 ROS 节点:命名(唯一)
  12. 3.实例化 ROS 句柄
  13. 4.实例化 订阅者 对象
  14. 5.处理订阅的消息(回调函数)
  15. 6.设置循环调用回调函数
  16. */
  17. // 1.包含头文件
  18. #include "ros/ros.h"
  19. #include "std_msgs/String.h"
  20. //回调函数 (参数是订阅到的消息) 消息是std_msgs::String类型,ConstPt常量指针的引用
  21. //msg_p是指针变量
  22. void doMsg(const std_msgs::String::ConstPtr& msg_p){
  23. //通过msg获取并操作订阅到的数据
  24. ROS_INFO("我听见:%s",msg_p->data.c_str());
  25. // ROS_INFO("我听见:%s",(*msg_p).data.c_str());
  26. }
  27. int main(int argc, char *argv[])
  28. {
  29. setlocale(LC_ALL,"");
  30. //2.初始化 ROS 节点:命名(唯一)
  31. ros::init(argc,argv,"listener");
  32. //3.实例化 ROS 句柄
  33. ros::NodeHandle nh;
  34. //4.实例化 订阅者 对象 (话题chatter要和发布方的一致)
  35. ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
  36. //5.处理订阅的消息(回调函数)
  37. // 6.设置循环调用回调函数
  38. ros::spin();//循环读取接收的数据,并调用回调函数处理
  39. return 0;
  40. }

注:关键点在于chatter这个话题要保持一致!!!!

3.配置 CMakeLists.txt

  1. add_executable(Hello_pub
  2. src/Hello_pub.cpp
  3. )
  4. add_executable(Hello_sub
  5. src/Hello_sub.cpp
  6. )
  7. target_link_libraries(Hello_pub
  8. ${catkin_LIBRARIES}
  9. )
  10. target_link_libraries(Hello_sub
  11. ${catkin_LIBRARIES}
  12. )

4.执行

1.启动 roscore;

2.启动发布节点;

3.启动订阅节点。

运行结果与引言部分的演示案例2类似。
PS:可以使用 rqt_graph 查看节点关系。

5.注意

补充0:

vscode 中的 main 函数 声明 int main(int argc, char const *argv[]){},默认生成 argv 被 const 修饰,需要去除该修饰符

补充1:

ros/ros.h No such file or directory ….. 检查 CMakeList.txt find_package 出现重复,删除内容少的即可 参考资料:https://answers.ros.org/question/237494/fatal-error-rosrosh-no-such-file-or-directory/ 补充2: find_package 不添加一些包,也可以运行啊, ros.wiki 答案如下
  1. You may notice that sometimes your project builds fine even if you did not call find_package with all dependencies. This is because catkin combines all your projects into one, so if an earlier project calls find_package, yours is configured with the same values. But forgetting the call means your project can easily break when built in isolation.

补充3:

订阅时,第一条数据丢失 原因: 发送第一条数据时, publisher 还未在 roscore 注册完毕 解决: 注册后,加入休眠 ros::Duration(3.0).sleep(); 延迟第一条数据的发送

第二章、ROS通信机制 - 图2或者

第二章、ROS通信机制 - 图3


PS:可以使用 rqt_graph 查看节点关系。

2.2、话题通信基本操作B(Py)

流程:

  1. 编写发布方实现;
  2. 编写订阅方实现;
  3. 为python文件添加可执行权限;
  4. 编辑配置文件;
  5. 编译并执行。

1.发布方

  1. #! /usr/bin/env python
  2. """
  3. 需求: 实现基本的话题通信,一方发布数据,一方接收数据,
  4. 实现的关键点:
  5. 1.发送方
  6. 2.接收方
  7. 3.数据(此处为普通文本)
  8. PS: 二者需要设置相同的话题
  9. 消息发布方:
  10. 循环发布信息:HelloWorld 后缀数字编号
  11. 实现流程:
  12. 1.导包
  13. 2.初始化 ROS 节点:命名(唯一)
  14. 3.实例化 发布者 对象
  15. 4.组织被发布的数据,并编写逻辑发布数据
  16. """
  17. #1.导包
  18. import rospy
  19. from std_msgs.msg import String //导入数据类型
  20. if __name__ == "__main__":
  21. #2.初始化 ROS 节点:命名(唯一)
  22. rospy.init_node("talker_p")
  23. #3.实例化 发布者 对象
  24. pub = rospy.Publisher("chatter",String,queue_size=10)
  25. #4.组织被发布的数据,并编写逻辑发布数据
  26. msg = String() #创建 msg 对象
  27. msg_front = "hello 你好"
  28. count = 0 #计数器
  29. # 设置循环频率
  30. rate = rospy.Rate(1)
  31. while not rospy.is_shutdown():
  32. #拼接字符串
  33. msg.data = msg_front + str(count)
  34. pub.publish(msg)
  35. rate.sleep()
  36. rospy.loginfo("写出的数据:%s",msg.data)
  37. count += 1

2.订阅方

  1. #! /usr/bin/env python
  2. """
  3. 需求: 实现基本的话题通信,一方发布数据,一方接收数据,
  4. 实现的关键点:
  5. 1.发送方
  6. 2.接收方
  7. 3.数据(此处为普通文本)
  8. 消息订阅方:
  9. 订阅话题并打印接收到的消息
  10. 实现流程:
  11. 1.导包
  12. 2.初始化 ROS 节点:命名(唯一)
  13. 3.实例化 订阅者 对象
  14. 4.处理订阅的消息(回调函数)
  15. 5.设置循环调用回调函数
  16. """
  17. #1.导包
  18. import rospy
  19. from std_msgs.msg import String
  20. def doMsg(msg):
  21. rospy.loginfo("I heard:%s",msg.data)
  22. if __name__ == "__main__":
  23. #2.初始化 ROS 节点:命名(唯一)
  24. rospy.init_node("listener_p")
  25. #3.实例化 订阅者 对象
  26. sub = rospy.Subscriber("chatter",String,doMsg,queue_size=10)
  27. #4.处理订阅的消息(回调函数)
  28. #5.设置循环调用回调函数
  29. rospy.spin()

3.添加可执行权限

4.配置 CMakeLists.txt

5.执行

注:c++版本发布与py版本的订阅可以兼容,只要保证话题名称一样就好。

3、话题通信自定义msg

在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:StringInt32Int64CharBoolEmpty.… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性(精辟),当传输一些复杂的数据,比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint*)
  • float32, float64
  • string
  • time, duration
  • other msg files
  • variable-length array[] and fixed-length array[C]
ROS中还有一种特殊类型:Header**标头包含时间戳和ROS中常用的坐标帧信息**。会经常看到msg文件的第一行具有Header标头

需求:创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。

流程:

  1. 1. **<font style="color:#F5222D;">按照固定格式创建 msg 文件</font>**
  2. 2. **<font style="color:#F5222D;">编辑配置文件</font>**
  3. 3. **<font style="color:#F5222D;">编译生成可以被 Python C++ 调用的中间文件</font>**

1.定义msg文件

功能包下新建 msg 文件目录,添加文件 Person.msg
  1. string name
  2. uint16 age
  3. float64 height

2.编辑配置文件

package.xml中添加编译依赖与执行依赖(2步)

  1. <build_depend>message_generation</build_depend> //编译时依赖
  2. <exec_depend>message_runtime</exec_depend> //运行时依赖
  3. <!--
  4. exce_depend 以前对应的是 run_depend 现在非法
  5. -->

CMakeLists.txt编辑 msg 相关配置(4步)

  1. find_package(catkin REQUIRED COMPONENTS
  2. roscpp
  3. rospy
  4. std_msgs
  5. message_generation
  6. )
  7. # 需要加入 message_generation,必须有 std_msgs
  1. ## 配置 msg 源文件
  2. add_message_files(
  3. FILES
  4. Person.msg
  5. )
  1. # 生成消息时依赖于 std_msgs
  2. generate_messages(
  3. DEPENDENCIES
  4. std_msgs
  5. )
  1. #执行时依赖
  2. catkin_package(
  3. # INCLUDE_DIRS include
  4. # LIBRARIES demo02_talker_listener
  5. CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
  6. # DEPENDS system_lib
  7. )

3.编译

编译后的中间文件查看:

C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)
第二章、ROS通信机制 - 图4 Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/msg)

第二章、ROS通信机制 - 图5

后续调用相关 msg 时,是从这些中间文件调用的

4、话题通信自定义msg调用A(C++)

流程:

  1. 1. <font style="color:rgb(51, 51, 51);">编写发布方实现;</font>
  2. 2. <font style="color:rgb(51, 51, 51);">编写订阅方实现;</font>
  3. 3. <font style="color:rgb(51, 51, 51);">编辑配置文件;</font>
  4. 4. <font style="color:rgb(51, 51, 51);">编译并执行。</font>

0.vscode 配置

为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:
  1. {
  2. "configurations": [
  3. {
  4. "browse": {
  5. "databaseFilename": "",
  6. "limitSymbolsToIncludedHeaders": true
  7. },
  8. "includePath": [
  9. "/opt/ros/noetic/include/**",
  10. "/usr/include/**",
  11. "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径
  12. ],
  13. "name": "ROS",
  14. "intelliSenseMode": "gcc-x64",
  15. "compilerPath": "/usr/bin/gcc",
  16. "cStandard": "c11",
  17. "cppStandard": "c++17"
  18. }
  19. ],
  20. "version": 4
  21. }

第二章、ROS通信机制 - 图6

1.发布方

  1. /*
  2. 需求: 循环发布人的信息
  3. */
  4. #include "ros/ros.h"
  5. #include "demo02_talker_listener/Person.h" //前面就是功能包的名称
  6. int main(int argc, char *argv[])
  7. {
  8. setlocale(LC_ALL,"");
  9. //1.初始化 ROS 节点
  10. ros::init(argc,argv,"talker_person");
  11. //2.创建 ROS 句柄
  12. ros::NodeHandle nh;
  13. //3.创建发布者对象
  14. ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);
  15. //4.组织被发布的消息,编写发布逻辑并发布消息
  16. demo02_talker_listener::Person p;
  17. p.name = "sunwukong";
  18. p.age = 2000;
  19. p.height = 1.45;
  20. ros::Rate r(1);
  21. while (ros::ok())
  22. {
  23. pub.publish(p);
  24. p.age += 1;
  25. ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);
  26. r.sleep();
  27. ros::spinOnce(); //建议
  28. }
  29. return 0;
  30. }

2.订阅方

  1. /*
  2. 需求: 订阅人的信息
  3. */
  4. #include "ros/ros.h"
  5. #include "demo02_talker_listener/Person.h" //前面就是功能包的名称
  6. //回调函数
  7. void doPerson(const demo02_talker_listener::Person::ConstPtr& person_p){
  8. ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height);
  9. }
  10. int main(int argc, char *argv[])
  11. {
  12. setlocale(LC_ALL,"");
  13. //1.初始化 ROS 节点
  14. ros::init(argc,argv,"listener_person");
  15. //2.创建 ROS 句柄
  16. ros::NodeHandle nh;
  17. //3.创建订阅对象
  18. ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson);
  19. //4.回调函数中处理 person
  20. //5.ros::spin();
  21. ros::spin();
  22. return 0;
  23. }

3.配置 CMakeLists.txt(自定义的消息类型有三处修改)

需要添加 add_dependencies** 用以设置所依赖的消息相关的中间文件。保证调用时候的依赖关系**
python add_executable(person_talker src/person_talker.cpp) add_executable(person_listener src/person_listener.cpp) add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp) //保证调用时候的依赖关系 add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp) target_link_libraries(person_talker ${catkin_LIBRARIES} ) target_link_libraries(person_listener ${catkin_LIBRARIES} ) #### 4.执行 1.启动 roscore; 2.启动发布节点; 3.启动订阅节点。 运行结果与引言部分的演示案例2类似。

5、话题通信自定义msg调用B(Py)

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
  1. 1. <font style="color:rgb(51, 51, 51);">发布方</font>
  2. 2. <font style="color:rgb(51, 51, 51);">接收方</font>
  3. 3. <font style="color:rgb(51, 51, 51);">数据(此处为自定义消息)</font>

流程:

  1. 1. <font style="color:rgb(51, 51, 51);">编写发布方实现;</font>
  2. 2. <font style="color:rgb(51, 51, 51);">编写订阅方实现;</font>
  3. 3. <font style="color:rgb(51, 51, 51);">为python文件添加可执行权限;</font>
  4. 4. <font style="color:rgb(51, 51, 51);">编辑配置文件;</font>
  5. 5. <font style="color:rgb(51, 51, 51);">编译并执行。</font>

0.vscode配置 (自定义消息会多出这一步)

为了方便代码提示以及误抛异常,需要先配置 vscode,将前面生成的 python 文件路径配置进 settings.json
  1. {
  2. "python.autoComplete.extraPaths": [
  3. "/opt/ros/noetic/lib/python3/dist-packages",
  4. "/xxx/yyy工作空间/devel/lib/python3/dist-packages"
  5. ]
  6. }

1.发布方

  1. #! /usr/bin/env python
  2. """
  3. 发布方:
  4. 循环发送消息
  5. """
  6. import rospy
  7. from demo02_talker_listener.msg import Person //前面就是功能包的名称
  8. if __name__ == "__main__":
  9. #1.初始化 ROS 节点
  10. rospy.init_node("talker_person_p")
  11. #2.创建发布者对象
  12. pub = rospy.Publisher("chatter_person",Person,queue_size=10)
  13. #3.组织消息
  14. p = Person()
  15. p.name = "葫芦瓦"
  16. p.age = 18
  17. p.height = 0.75
  18. #4.编写消息发布逻辑
  19. rate = rospy.Rate(1)
  20. while not rospy.is_shutdown():
  21. pub.publish(p) #发布消息
  22. rate.sleep() #休眠
  23. rospy.loginfo("姓名:%s, 年龄:%d, 身高:%.2f",p.name, p.age, p.height)

2.订阅方

  1. #! /usr/bin/env python
  2. """
  3. 订阅方:
  4. 订阅消息
  5. """
  6. import rospy
  7. from demo02_talker_listener.msg import Person //前面就是功能包的名称
  8. def doPerson(p):
  9. rospy.loginfo("接收到的人的信息:%s, %d, %.2f",p.name, p.age, p.height)
  10. if __name__ == "__main__":
  11. #1.初始化节点
  12. rospy.init_node("listener_person_p")
  13. #2.创建订阅者对象
  14. sub = rospy.Subscriber("chatter_person",Person,doPerson,queue_size=10)
  15. rospy.spin() #4.循环

3.权限设置

4.配置 CMakeLists.txt (操作一致)

  1. catkin_install_python(PROGRAMS
  2. scripts/talker_p.py
  3. scripts/listener_p.py
  4. scripts/person_talker.py
  5. scripts/person_listener.py
  6. DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
  7. )

5.执行

2、服务通信

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人… 此时需要拍摄照片并留存。

概念

以请求响应的方式实现不同节点之间数据交互的通信模式。

作用

用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。

案例

实现两个数字的求和,客户端节点,运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端。

1、服务通信理论模型

服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:
  1. - **<font style="color:rgb(51, 51, 51);">ROS master(管理者)</font>**
  2. - **<font style="color:#F5222D;">Server(服务端)</font>**
  3. - **<font style="color:#F5222D;">Client(客户端)</font>**
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

第二章、ROS通信机制 - 图7

整个流程由以下步骤实现:

0.Server注册

Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

1.Client注册

Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

2.ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP** **地址信息。

3.Client发送请求

Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

4.Server发送响应

Server 接收、解析请求的数据,并产生响应结果返回给 Client。 注意: 1.客户端请求被处理时,需要保证服务器已经启动; 2.服务端和客户端都可以存在多个。 3.先RPC在TCP

2、服务通信自定义srv

流程:

srv 文件内的可用数据类型与 msg 文件一致,**定义 srv 实现流程与自定义 msg 实现流程类似**:
  1. 1. **<font style="color:rgb(51, 51, 51);">按照固定格式创建srv文件</font>**
  2. 2. **<font style="color:rgb(51, 51, 51);">编辑配置文件</font>**
  3. 3. **<font style="color:rgb(51, 51, 51);">编译生成中间文件</font>**

1.定义srv文件(和msg文件类似)

服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用—-分割,具体实现如下: 功能包下新建 srv 目录,添加 xxx.srv 文件,内容:
  1. 客户端请求时发送的两个数字
  2. int32 num1
  3. int32 num2
  4. ---
  5. # 服务器响应发送的数据
  6. int32 sum

2.编辑配置文件

(1)package.xml中添加编译依赖与执行依赖 (与msg操作一致,一个功能包下无需重复添加)

  1. <build_depend>message_generation</build_depend>
  2. <exec_depend>message_runtime</exec_depend>
  3. <!--
  4. exce_depend 以前对应的是 run_depend 现在非法
  5. -->

(2)CMakeLists.txt编辑 srv 相关配置 (与msg操作一致,少了一个执行时依赖,一个功能包下无需重复添加)

  1. find_package(catkin REQUIRED COMPONENTS
  2. roscpp
  3. rospy
  4. std_msgs
  5. message_generation
  6. )
  7. # 需要加入 message_generation,必须有 std_msgs
  1. add_service_files(
  2. FILES
  3. AddInts.srv
  4. )

注意:这个地方与msg不在一个地方

第二章、ROS通信机制 - 图8第二章、ROS通信机制 - 图9

  1. #生成消息是依赖于 std_msgs
  2. generate_messages(
  3. DEPENDENCIES
  4. std_msgs
  5. )
注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以

3.编译

编译后的中间文件查看:(多了这三个.srv文件) C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h) (与msg一致)**
第二章、ROS通信机制 - 图10 Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/srv) (与msg一致)
**第二章、ROS通信机制 - 图11 后续调用相关 srv 时,是从这些中间文件调用的

3、服务通信自定义srv调用 (c++)

需求:

编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
  1. 1. <font style="color:rgb(51, 51, 51);">服务端</font>
  2. 2. <font style="color:rgb(51, 51, 51);">客户端</font>
  3. 3. <font style="color:rgb(51, 51, 51);">数据</font>

流程:

  1. 1. <font style="color:rgb(51, 51, 51);">编写服务端实现;</font>
  2. 2. <font style="color:rgb(51, 51, 51);">编写客户端实现;</font>
  3. 3. <font style="color:rgb(51, 51, 51);">编辑配置文件;</font>
  4. 4. <font style="color:rgb(51, 51, 51);">编译并执行。</font>

0.vscode配置

需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同: python { "configurations": [ { "browse": { "databaseFilename": "", "limitSymbolsToIncludedHeaders": true }, "includePath": [ "/opt/ros/noetic/include/**", "/usr/include/**", "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17" } ], "version": 4 } #### 1.服务端 python /* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 服务 对象 5.回调函数处理请求并产生响应 6.由于请求有多个,需要调用 ros::spin() */ #include "ros/ros.h" #include "demo03_server_client/AddInts.h" // bool 返回值由于标志是否处理成功 doReq返回值是布尔类型 bool doReq(demo03_server_client::AddInts::Request& req, demo03_server_client::AddInts::Response& resp){ int num1 = req.num1; int num2 = req.num2; ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2); //逻辑处理 if (num1 < 0 || num2 < 0) { ROS_ERROR("提交的数据异常:数据不可以为负数"); return false; } //如果没有异常,那么相加并将结果赋值给 resp resp.sum = num1 + num2; return true; } int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Server"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 服务 对象 ros::ServiceServer server = nh.advertiseService("AddInts",doReq);// ROS_INFO("服务已经启动...."); // 5.回调函数处理请求并产生响应 // 6.由于请求有多个,需要调用 ros::spin() ros::spin(); return 0; } 调试的时候:**rosservice call [服务的节点名称] tab补齐修改参数(加个空格补齐) 第二章、ROS通信机制 - 图12 #### 2.客户端 python /* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 客户端 对象 5.请求服务,接收响应 */ // 1.包含头文件 #include "ros/ros.h" #include "demo03_server_client/AddInts.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3 if (argc != 3) // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径) { ROS_ERROR("请提交两个整数"); return 1; } // 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Client"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 客户端 对象 <模板> ("话题名称保证和服务器端的一致") ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts"); //等待服务启动成功 //方式1 ros::service::waitForService("AddInts"); //方式2 // client.waitForExistence(); // 5.组织请求数据 demo03_server_client::AddInts ai; //提交数据的是创建实例化对象 ai.request.num1 = atoi(argv[1]); ai.request.num2 = atoi(argv[2]); // 6.发送请求,返回 bool 值,标记是否成功 bool flag = client.call(ai); //call里面就传递server对象就行。客户端访问服务器并且把访问对象ai提交. //ai对象里面有请求和响应的值 //返回结果如果是true的话就表示能正常处理。 // 7.处理响应 if (flag) { ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum); } else { ROS_ERROR("请求处理失败...."); return 1; } return 0; } 为什么argc参数是3呢???运行后发现:第一个参数是路径客户端名字 第二章、ROS通信机制 - 图13 #### 3.配置 CMakeLists.txt (与官网上有所区别_gencpp) python add_executable(AddInts_Server src/AddInts_Server.cpp) add_executable(AddInts_Client src/AddInts_Client.cpp) add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp) add_dependencies(AddInts_Client ${PROJECT_NAME}_gencp) target_link_libraries(AddInts_Server ${catkin_LIBRARIES} ) target_link_libraries(AddInts_Client ${catkin_LIBRARIES} ) #### 4.执行 流程:** + 需要先启动服务:rosrun 包名 服务 + 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

会根据提交的数据响应相加后的结果。

注意:

如果先启动客户端,那么会导致运行失败。解决:在ROS中已经内置了相关的函数,在客户端启动后挂起,等待服务端启动。函数调用的额时机是在客户端创建之后,请求发送之前。

优化:

在客户端发送请求前添加:**client.waitForExistence();**

或:**ros::service::waitForService(“AddInts”);**

这是一个阻塞式函数,只有服务启动成功后才会继续执行

此处可以使用 launch 文件优化,但是需要注意 args 传参特点

4、服务通信自定义srv调用 (py)

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
  1. 服务端
  2. 客户端
  3. 数据

流程:

  1. 编写服务端实现;
  2. 编写客户端实现;
  3. 为python文件添加可执行权限;
  4. 编辑配置文件;
  5. 编译并执行。

0.vscode配置

需要像之前自定义 msg 实现一样配置settings.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同: python { "python.autoComplete.extraPaths": [ "/opt/ros/noetic/lib/python3/dist-packages", ] } #### 1.服务端 python #! /usr/bin/env python """ 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 服务器端实现: 1.导包 2.初始化 ROS 节点 3.创建服务对象 4.回调函数处理请求并产生响应 5.spin 函数 """ # 1.导包 import rospy from demo03_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse //from demo03_server_client.srv import * //更简单 # 回调函数的参数是请求对象,返回值是响应对象 def doReq(req): # 解析提交的数据 sum = req.num1 + req.num2 rospy.loginfo("提交的数据:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum) # 创建响应对象,赋值并返回 # resp = AddIntsResponse() # resp.sum = sum resp = AddIntsResponse(sum) return resp if __name__ == "__main__": # 2.初始化 ROS 节点 rospy.init_node("addints_server_p") # 3.创建服务对象 server = rospy.Service("AddInts",AddInts,doReq) # 4.回调函数处理请求并产生响应 # 5.spin 函数 rospy.spin() #### 2.客户端 python #! /usr/bin/env python """ 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析 客户端实现: 1.导包 2.初始化 ROS 节点 3.创建请求对象 4.发送请求 5.接收并处理响应 优化: 加入数据的动态获取 """ #1.导包 import rospy from demo03_server_client.srv import * import sys if __name__ == "__main__": #优化实现 if len(sys.argv) != 3: rospy.logerr("请正确提交参数") sys.exit(1) # 2.初始化 ROS 节点 rospy.init_node("AddInts_Client_p") # 3.创建请求对象 client = rospy.ServiceProxy("AddInts",AddInts) # 请求前,等待服务已经就绪 # 方式1: # rospy.wait_for_service("AddInts") # 方式2 client.wait_for_service() # 4.发送请求,接收并处理响应 # 方式1 # resp = client(3,4) # 方式2 # resp = client(AddIntsRequest(1,5)) # 方式3 req = AddIntsRequest() # req.num1 = 100 # req.num2 = 200 #优化 req.num1 = int(sys.argv[1]) req.num2 = int(sys.argv[2]) resp = client.call(req) rospy.loginfo("响应结果:%d",resp.sum) #### 3.设置权限 终端下进入 scripts 执行:chmod +x *.py

4.配置 CMakeLists.txt

CMakeLists.txt

  1. catkin_install_python(PROGRAMS
  2. scripts/AddInts_Server_p.py
  3. scripts/AddInts_Client_p.py
  4. DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
  5. )

5.执行

流程:

  • 需要先启动服务:rosrun 包名 服务
  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

会根据提交的数据响应相加后的结果。

3、参数服务器

参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,关于参数服务器的典型应用场景如下:

导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径 上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:
  1. - <font style="color:rgb(51, 51, 51);">路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数</font>
参数服务器,一般适用于存在数据共享的一些应用场景。

概念

以共享的方式实现不同节点之间数据交互的通信模式。

作用

存储一些多节点共享的数据,类似于全局变量。

案例

实现参数操作。

1、参数服务器理论模型

参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (参数设置者)
  • Listener (参数调用者)
ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。

第二章、ROS通信机制 - 图14

整个流程由以下步骤实现:

1.Talker 设置参数

Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。

2.Listener 获取参数

Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。

3.ROS Master 向 Listener 发送参数值

ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。
参数可使用数据类型:
  • 32-bit integers
  • booleans
  • strings
  • doubles
  • iso8601 dates
  • lists
  • base64-encoded binary data
  • 字典
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数

2、参数服务操作(C++)

需求:实现参数服务器参数的增删改查操作。

在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:
  • ros::NodeHandle
  • ros::param
下面为具体操作演示

1.参数服务器新增(修改)参数

  1. /*
  2. 参数服务器操作之新增与修改(二者API一样)_C++实现:
  3. roscpp 中提供了两套 API 实现参数操作
  4. ros::NodeHandle // (第一套)
  5. setParam("键",值)
  6. ros::param // (第二套)
  7. set("键","值")
  8. 示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数
  9. 修改(相同的键,不同的值)
  10. */
  11. #include "ros/ros.h"
  12. int main(int argc, char *argv[])
  13. {
  14. ros::init(argc,argv,"set_update_param"); 初始化节点
  15. std::vector<std::string> stus; //实例化消息类型成stus
  16. stus.push_back("zhangsan");
  17. stus.push_back("李四");
  18. stus.push_back("王五");
  19. stus.push_back("孙大脑袋");
  20. std::map<std::string,std::string> friends;
  21. friends["guo"] = "huang";
  22. friends["yuang"] = "xiao";
  23. //NodeHandle--------------------------------------------------------
  24. ros::NodeHandle nh;
  25. nh.setParam("nh_int",10); //整型
  26. nh.setParam("nh_double",3.14); //浮点型
  27. nh.setParam("nh_bool",true); //bool
  28. nh.setParam("nh_string","hello NodeHandle"); //字符串
  29. nh.setParam("nh_vector",stus); // vector
  30. nh.setParam("nh_map",friends); // map
  31. //修改演示(相同的键,不同的值)
  32. nh.setParam("nh_int",10000);
  33. //param--------------------------------------------------------
  34. ros::param::set("param_int",20);
  35. ros::param::set("param_double",3.14);
  36. ros::param::set("param_string","Hello Param");
  37. ros::param::set("param_bool",false);
  38. ros::param::set("param_vector",stus);
  39. ros::param::set("param_map",friends);
  40. //修改演示(相同的键,不同的值)
  41. ros::param::set("param_int",20000);
  42. return 0;
  43. }

2.参数服务器获取参数

  1. /*
  2. 参数服务器操作之查询_C++实现:
  3. roscpp 中提供了两套 API 实现参数操作
  4. ros::NodeHandle
  5. param(键,默认值)
  6. 存在,返回对应结果,否则返回默认值
  7. getParam(键,存储结果的变量)
  8. 存在,返回 true,且将值赋值给参数2
  9. 若果键不存在,那么返回值为 false,且不为参数2赋值
  10. getParamCached键,存储结果的变量)--提高变量获取效率
  11. 存在,返回 true,且将值赋值给参数2
  12. 若果键不存在,那么返回值为 false,且不为参数2赋值
  13. getParamNames(std::vector<std::string>)
  14. 获取所有的键,并存储在参数 vector
  15. hasParam(键)
  16. 是否包含某个键,存在返回 true,否则返回 false
  17. searchParam(参数1,参数2)
  18. 搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
  19. ros::param ----- NodeHandle 类似
  20. */
  21. #include "ros/ros.h"
  22. int main(int argc, char *argv[])
  23. {
  24. setlocale(LC_ALL,"");
  25. ros::init(argc,argv,"get_param");
  26. //NodeHandle--------------------------------------------------------
  27. /*
  28. ros::NodeHandle nh;
  29. // param 函数
  30. int res1 = nh.param("nh_int",100); // 键存在
  31. int res2 = nh.param("nh_int2",100); // 键不存在
  32. ROS_INFO("param获取结果:%d,%d",res1,res2);
  33. // getParam 函数
  34. int nh_int_value;
  35. double nh_double_value;
  36. bool nh_bool_value;
  37. std::string nh_string_value;
  38. std::vector<std::string> stus;
  39. std::map<std::string, std::string> friends;
  40. nh.getParam("nh_int",nh_int_value);
  41. nh.getParam("nh_double",nh_double_value);
  42. nh.getParam("nh_bool",nh_bool_value);
  43. nh.getParam("nh_string",nh_string_value);
  44. nh.getParam("nh_vector",stus);
  45. nh.getParam("nh_map",friends);
  46. ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
  47. nh_int_value,
  48. nh_double_value,
  49. nh_string_value.c_str(),
  50. nh_bool_value
  51. );
  52. for (auto &&stu : stus)
  53. {
  54. ROS_INFO("stus 元素:%s",stu.c_str());
  55. }
  56. for (auto &&f : friends)
  57. {
  58. ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
  59. }
  60. // getParamCached()
  61. nh.getParamCached("nh_int",nh_int_value);
  62. ROS_INFO("通过缓存获取数据:%d",nh_int_value);
  63. //getParamNames()
  64. std::vector<std::string> param_names1;
  65. nh.getParamNames(param_names1);
  66. for (auto &&name : param_names1)
  67. {
  68. ROS_INFO("名称解析name = %s",name.c_str());
  69. }
  70. ROS_INFO("----------------------------");
  71. ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int"));
  72. ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt"));
  73. std::string key;
  74. nh.searchParam("nh_int",key);
  75. ROS_INFO("搜索键:%s",key.c_str());
  76. */
  77. //param--------------------------------------------------------
  78. ROS_INFO("++++++++++++++++++++++++++++++++++++++++");
  79. int res3 = ros::param::param("param_int",20); //存在
  80. int res4 = ros::param::param("param_int2",20); // 不存在返回默认
  81. ROS_INFO("param获取结果:%d,%d",res3,res4);
  82. // getParam 函数
  83. int param_int_value;
  84. double param_double_value;
  85. bool param_bool_value;
  86. std::string param_string_value;
  87. std::vector<std::string> param_stus;
  88. std::map<std::string, std::string> param_friends;
  89. ros::param::get("param_int",param_int_value);
  90. ros::param::get("param_double",param_double_value);
  91. ros::param::get("param_bool",param_bool_value);
  92. ros::param::get("param_string",param_string_value);
  93. ros::param::get("param_vector",param_stus);
  94. ros::param::get("param_map",param_friends);
  95. ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
  96. param_int_value,
  97. param_double_value,
  98. param_string_value.c_str(),
  99. param_bool_value
  100. );
  101. for (auto &&stu : param_stus)
  102. {
  103. ROS_INFO("stus 元素:%s",stu.c_str());
  104. }
  105. for (auto &&f : param_friends)
  106. {
  107. ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
  108. }
  109. // getParamCached()
  110. ros::param::getCached("param_int",param_int_value);
  111. ROS_INFO("通过缓存获取数据:%d",param_int_value);
  112. //getParamNames()
  113. std::vector<std::string> param_names2;
  114. ros::param::getParamNames(param_names2);
  115. for (auto &&name : param_names2)
  116. {
  117. ROS_INFO("名称解析name = %s",name.c_str());
  118. }
  119. ROS_INFO("----------------------------");
  120. ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int"));
  121. ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));
  122. std::string key;
  123. ros::param::search("param_int",key);
  124. ROS_INFO("搜索键:%s",key.c_str());
  125. return 0;
  126. }

3.参数服务器删除参数

  1. /*
  2. 参数服务器操作之删除_C++实现:
  3. ros::NodeHandle
  4. deleteParam("键")
  5. 根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
  6. ros::param
  7. del("键")
  8. 根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
  9. */
  10. #include "ros/ros.h"
  11. int main(int argc, char *argv[])
  12. {
  13. setlocale(LC_ALL,"");
  14. ros::init(argc,argv,"delete_param");
  15. ros::NodeHandle nh;
  16. bool r1 = nh.deleteParam("nh_int");
  17. ROS_INFO("nh 删除结果:%d",r1);
  18. bool r2 = ros::param::del("param_int");
  19. ROS_INFO("param 删除结果:%d",r2);
  20. return 0;
  21. }

3、参数服务操作(py)

1.参数服务器新增(修改)参数

  1. #! /usr/bin/env python
  2. """
  3. 参数服务器操作之新增与修改(二者API一样)_Python实现:
  4. """
  5. import rospy
  6. if __name__ == "__main__":
  7. rospy.init_node("set_update_paramter_p")
  8. # 设置各种类型参数
  9. rospy.set_param("p_int",10)
  10. rospy.set_param("p_double",3.14)
  11. rospy.set_param("p_bool",True)
  12. rospy.set_param("p_string","hello python")
  13. rospy.set_param("p_list",["hello","haha","xixi"])
  14. rospy.set_param("p_dict",{"name":"hulu","age":8})
  15. # 修改
  16. rospy.set_param("p_int",100)

2.参数服务器获取参数

  1. #! /usr/bin/env python
  2. """
  3. 参数服务器操作之查询_Python实现:
  4. get_param(键,默认值)
  5. 当键存在时,返回对应的值,如果不存在返回默认值
  6. get_param_cached
  7. get_param_names
  8. has_param
  9. search_param
  10. """
  11. import rospy
  12. if __name__ == "__main__":
  13. rospy.init_node("get_param_p")
  14. #获取参数
  15. int_value = rospy.get_param("p_int",10000)
  16. double_value = rospy.get_param("p_double")
  17. bool_value = rospy.get_param("p_bool")
  18. string_value = rospy.get_param("p_string")
  19. p_list = rospy.get_param("p_list")
  20. p_dict = rospy.get_param("p_dict")
  21. rospy.loginfo("获取的数据:%d,%.2f,%d,%s",
  22. int_value,
  23. double_value,
  24. bool_value,
  25. string_value)
  26. for ele in p_list:
  27. rospy.loginfo("ele = %s", ele)
  28. rospy.loginfo("name = %s, age = %d",p_dict["name"],p_dict["age"])
  29. # get_param_cached
  30. int_cached = rospy.get_param_cached("p_int")
  31. rospy.loginfo("缓存数据:%d",int_cached)
  32. # get_param_names
  33. names = rospy.get_param_names()
  34. for name in names:
  35. rospy.loginfo("name = %s",name)
  36. rospy.loginfo("-"*80)
  37. # has_param
  38. flag = rospy.has_param("p_int")
  39. rospy.loginfo("包含p_int吗?%d",flag)
  40. # search_param
  41. key = rospy.search_param("p_int")
  42. rospy.loginfo("搜索的键 = %s",key)

3.参数服务器删除参数

  1. #! /usr/bin/env python
  2. """
  3. 参数服务器操作之删除_Python实现:
  4. rospy.delete_param("键")
  5. 键存在时,可以删除成功,键不存在时,会抛出异常
  6. """
  7. import rospy
  8. if __name__ == "__main__":
  9. rospy.init_node("delete_param_p")
  10. try:
  11. rospy.delete_param("p_int")
  12. except Exception as e:
  13. rospy.loginfo("删除失败")

4、常用命令

机器人系统中启动的节点少则几个,多则十几个、几十个,不同的节点名称各异,通信时使用话题、服务、消息、参数等等都各不相同,一个显而易见的问题是: 当需要自定义节点和其他某个已经存在的节点通信时,如何获取对方的话题、以及消息载体的格式呢?

在 ROS 同提供了一些实用的命令行工具,可以用于获取不同节点的各类信息,常用的命令如下:
  • rosnode : 操作节点
  • rostopic : 操作话题
  • rosservice : 操作服务
  • rosmsg : 操作msg消息
  • rossrv : 操作srv消息
  • rosparam : 操作参数

作用

和之前介绍的文件系统操作命令比较,文件操作命令是静态的,操作的是磁盘上的文件,而上述命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息。

案例

本节将借助于2.1、2.2和2.3的通信实现介绍相关命令的基本使用,并通过练习ROS内置的小海龟例程来强化命令的应用。

1、rosnode

rosnode 是用于获取节点信息的命令
  1. rosnode ping 测试到节点的连接状态 (用过√)
  2. rosnode list 列出活动节点 (用过√)
  3. rosnode info 打印节点信息 (用过√)
  4. rosnode machine 列出指定设备上节点 (用过√)
  5. rosnode kill 杀死某个节点
  6. rosnode cleanup 清除不可连接的节点
  • rosnode ping测试到节点的连接状态
  • rosnode list列出活动节点
  • rosnode info打印节点信息
  • rosnode machine列出指定设备上的节点
  • rosnode kill杀死某个节点
  • rosnode cleanup清除无用节点,启动乌龟节点,然后 ctrl + c 关闭,该节点并没被彻底清除,可以使用 cleanup 清除节点

2、rostopic

rostopic包含rostopic命令行工具,用于显示有关ROS 主题的调试信息,包括发布者,订阅者,发布频率和ROS消息。它还包含一个实验性Python库,用于动态获取有关主题的信息并与之交互。

  1. rostopic bw 显示主题使用的带宽
  2. rostopic delay 显示带有 header 的主题延迟
  3. rostopic echo 打印消息到屏幕
  4. rostopic find 根据类型查找主题
  5. rostopic hz 显示主题的发布频率
  6. rostopic info 显示主题相关信息
  7. rostopic list 显示所有活动状态下的主题
  8. rostopic pub 将数据发布到主题
  9. rostopic type 打印主题类型
  • rostopic list(-v)直接调用即可,控制台将打印当前运行状态下的主题名称rostopic list -v : 获取话题详情(比如列出:发布者和订阅者个数…)
  • rostopic pub可以直接调用命令向订阅者发布消息为roboware 自动生成的 发布/订阅 模型案例中的 订阅者 发布一条字符串
  1. rostopic pub /主题名称 消息类型 消息内容
  2. rostopic pub /chatter std_msgs gagaxixi
为 小乌龟案例的 订阅者 发布一条运动信息
  1. rostopic pub /turtle1/cmd_vel geometry_msgs/Twist
  2. "linear:
  3. x: 1.0
  4. y: 0.0
  5. z: 0.0
  6. angular:
  7. x: 0.0
  8. y: 0.0
  9. z: 2.0"
  10. //只发布一次运动信息
  11. rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist
  12. "linear:
  13. x: 1.0
  14. y: 0.0
  15. z: 0.0
  16. angular:
  17. x: 0.0
  18. y: 0.0
  19. z: 2.0"
  20. // 10HZ 的频率循环发送运动信息
  • rostopic echo获取指定话题当前发布的消息 **打印消息到屏幕**
  • rostopic info获取当前话题的小关信息消息类型发布者信息订阅者信息
  • rostopic type列出话题的消息类型
  • rostopic find 消息类型根据消息类型查找话题
  • rostopic delay列出消息头信息
  • rostopic hz列出消息发布频率
  • rostopic bw列出消息发布带宽

3、rosmsg

rosmsg是用于显示有关 ROS消息类型的 信息的命令行工具。

rosmsg 演示

  1. rosmsg show 显示消息描述
  2. rosmsg info 显示消息信息
  3. rosmsg list 列出所有消息
  4. rosmsg md5 显示 md5 加密后的消息
  5. rosmsg package 显示某个功能包下的所有消息
  6. rosmsg packages 列出包含消息的功能包
  • rosmsg list会列出当前 ROS 中的所有 msg
  • rosmsg packages列出包含消息的所有包
  • rosmsg package列出某个包下的所有msg
  1. //rosmsg package 包名
  2. rosmsg package turtlesim
  • rosmsg show 显示消息描述
  1. //rosmsg show 消息名称
  2. rosmsg show turtlesim/Pose
  3. 结果:
  4. float32 x
  5. float32 y
  6. float32 theta
  7. float32 linear_velocity
  8. float32 angular_velocity

4、rosservice

rosservice包含用于列出和查询ROSServices的rosservice命令行工具。

调用部分服务时,如果对相关工作空间没有配置 path,需要进入工作空间调用 source ./devel/setup.bash

  1. rosservice args 打印服务参数
  2. rosservice call 使用提供的参数调用服务
  3. rosservice find 按照服务类型查找服务
  4. rosservice info 打印有关服务的信息
  5. rosservice list 列出所有活动的服务
  6. rosservice type 打印服务类型
  7. rosservice uri 打印服务的 ROSRPC uri
  • rosservice list 列出所有活动的 service
  1. ~ rosservice list
  2. /clear
  3. /kill
  4. /listener/get_loggers
  5. /listener/set_logger_level
  6. /reset
  7. /rosout/get_loggers
  8. /rosout/set_logger_level
  9. /rostopic_4985_1578723066421/get_loggers
  10. /rostopic_4985_1578723066421/set_logger_level
  11. /rostopic_5582_1578724343069/get_loggers
  12. /rostopic_5582_1578724343069/set_logger_level
  13. /spawn
  14. /turtle1/set_pen
  15. /turtle1/teleport_absolute
  16. /turtle1/teleport_relative
  17. /turtlesim/get_loggers
  18. /turtlesim/set_logger_level
  • rosservice args打印服务参数
  1. rosservice args /spawn
  2. x y theta name
  • rosservice call 调用服务
为小乌龟的案例生成一只新的乌龟(新的话题) (新的乌龟的信息可以用rostopic list来查看)
  1. rosservice call /spawn "x: 1.0
  2. y: 2.0
  3. theta: 0.0
  4. name: 'xxx'"
  5. name: "xxx"
  6. //生成一只叫 xxx 的乌龟
  • rosservice find根据消息类型获取话题
  • rosservice info获取服务话题详情
  • rosservice type获取消息类型
  • rosservice uri获取服务器 uri

5、rossrv

rossrv是用于显示有关ROS服务类型的信息的命令行工具,与 rosmsg 使用语法高度雷同。

  1. rossrv show 显示服务消息详情
  2. rossrv info 显示服务消息相关信息
  3. rossrv list 列出所有服务信息
  4. rossrv md5 显示 md5 加密后的服务消息
  5. rossrv package 显示某个包下所有服务消息
  6. rossrv packages 显示包含服务消息的所有包
  • rossrv list会列出当前 ROS 中的所有 srv 消息
  • rossrv packages列出包含服务消息的所有包
  • rossrv package列出某个包下的所有msg
  1. //rossrv package 包名
  2. rossrv package turtlesim
  • rossrv show 显示消息描述
  1. //rossrv show 消息名称
  2. rossrv show turtlesim/Spawn
  3. 结果:
  4. float32 x
  5. float32 y
  6. float32 theta
  7. string name
  8. ---
  9. string name
  • rossrv info作用与 rossrv show 一致
  • rossrv md5对 service 数据使用 md5 校验(加密)

6、rosparam

rosparam包含rosparam命令行工具,用于使用YAML编码文件在参数服务器上获取和设置ROS参数。
  1. rosparam set 设置参数
  2. rosparam get 获取参数
  3. rosparam load 从外部文件加载参数
  4. rosparam dump 将参数写出到外部文件
  5. rosparam delete 删除参数
  6. rosparam list 列出所有参数
  • rosparam list 列出所有参数
  1. rosparam list
  2. //默认结果
  3. /rosdistro
  4. /roslaunch/uris/host_helloros_virtual_machine__42911
  5. /rosversion
  6. /run_id
  • rosparam set 设置参数
  1. rosparam set name huluwa
  2. //再次调用 rosparam list 结果
  3. /name
  4. /rosdistro
  5. /roslaunch/uris/host_helloros_virtual_machine__42911
  6. /rosversion
  7. /run_id
  • rosparam get 获取参数
  1. rosparam get name
  2. //结果
  3. huluwa
  • rosparam delete 删除参数
  1. rosparam delete name
  2. //结果
  3. //去除了name
  • rosparam load(先准备 yaml 文件) 从外部文件加载参数
  1. rosparam load xxx.yaml
  • rosparam dump 将参数写出到外部文件
  1. rosparam dump yyy.yaml

5、通信机制实践操作

本节主要是通过ROS内置的turtlesim案例,结合已经介绍ROS命令获取节点、话题、话题消息、服务、服务消息与参数的信息,最终再以编码的方式实现乌龟运动的控制、乌龟位姿的订阅、乌龟生成与乌龟窗体背景颜色的修改。

1、实操——话题(topic)发布(pub)

需求描述:编码实现乌龟运动控制,让小乌龟做圆周运动。

实现分析:

  1. 乌龟运动控制实现,关键节点有两个,一个是乌龟运动显示节点 turtlesim_node,另一个是控制节点,二者是订阅发布模式实现通信的,乌龟运动显示节点直接调用即可,运动控制节点之前是使用的 turtle_teleop_key通过键盘 控制,现在需要自定义控制节点。
  2. 控制节点自实现时,首先需要了解控制节点与显示节点通信使用的话题与消息,可以使用ros命令结合计算图来获取。
  3. 了解了话题与消息之后,通过 C++ 或 Python 编写运动控制节点,通过指定的话题,按照一定的逻辑发布消息即可。

实现流程:

  1. 通过计算图结合ros命令获取话题与消息信息。
  2. 编码实现运动控制节点。
  3. 启动 roscore、turtlesim_node 以及自定义的控制节点,查看运行结果。

1.话题与消息获取

准备:** **先启动键盘控制乌龟运动案例。

1.1话题获取

获取话题:/turtle1/cmd_vel

通过计算图查看话题,启动计算图:
  1. rqt_graph
或者通过 rostopic 列出话题:
  1. rostopic list

第二章、ROS通信机制 - 图15

1.2消息获取

有话题意味着有消息,获取消息类型:geometry_msgs/Twist

  1. rostopic type /turtle1/cmd_vel

获取消息格式:

  1. rosmsg info geometry_msgs/Twist

响应结果:

  1. geometry_msgs/Vector3 linear
  2. float64 x
  3. float64 y
  4. float64 z
  5. geometry_msgs/Vector3 angular
  6. float64 x
  7. float64 y
  8. float64 z

第二章、ROS通信机制 - 图16

linear(线速度) 下的xyz分别对应在x、y和z方向上的速度(单位是 m/s); angular(角速度)下的xyz分别对应x轴上的翻滚、y轴上俯仰和z轴上偏航的速度(单位是rad/s)。
详情请查看补充资料。

2.实现发布节点

创建功能包需要依赖的功能包: roscpp rospy std_msgs geometry_msgs 实现方案A: C++
  1. /*
  2. 编写 ROS 节点,控制小乌龟画圆
  3. 准备工作:
  4. 1.获取topic(已知: /turtle1/cmd_vel)
  5. 2.获取消息类型(已知: geometry_msgs/Twist)
  6. 3.运行前,注意先启动 turtlesim_node 节点
  7. 实现流程:
  8. 1.包含头文件
  9. 2.初始化 ROS 节点
  10. 3.创建发布者对象
  11. 4.循环发布运动控制消息
  12. */
  13. #include "ros/ros.h"
  14. #include "geometry_msgs/Twist.h"
  15. int main(int argc, char *argv[])
  16. {
  17. setlocale(LC_ALL,"");
  18. // 2.初始化 ROS 节点
  19. ros::init(argc,argv,"control");
  20. ros::NodeHandle nh;
  21. // 3.创建发布者对象 <模板泛型>(“节点名称(与官方的保持一致,可topic list查看)”,最大缓存队列长度数量)
  22. ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel",1000);
  23. // 4.循环发布运动控制消息
  24. //4-1.组织消息
  25. geometry_msgs::Twist msg; //实例化消息对象 格式与官方保持一致(可以 rosmsg info 该消息 查看各式)
  26. msg.linear.x = 1.0;
  27. msg.linear.y = 0.0;
  28. msg.linear.z = 0.0;
  29. msg.angular.x = 0.0;
  30. msg.angular.y = 0.0;
  31. msg.angular.z = 2.0;
  32. //4-2.设置发送频率
  33. ros::Rate r(10);
  34. //4-3.循环发送
  35. while (ros::ok())
  36. {
  37. pub.publish(msg);
  38. ros::spinOnce();
  39. }
  40. return 0;
  41. }

第二章、ROS通信机制 - 图17

配置文件此处略

实现方案B: Python

  1. #! /usr/bin/env python
  2. """
  3. 编写 ROS 节点,控制小乌龟画圆
  4. 准备工作:
  5. 1.获取topic(已知: /turtle1/cmd_vel)
  6. 2.获取消息类型(已知: geometry_msgs/Twist)
  7. 3.运行前,注意先启动 turtlesim_node 节点
  8. 实现流程:
  9. 1.导包
  10. 2.初始化 ROS 节点
  11. 3.创建发布者对象
  12. 4.循环发布运动控制消息
  13. """
  14. import rospy
  15. from geometry_msgs.msg import Twist
  16. if __name__ == "__main__":
  17. # 2.初始化 ROS 节点
  18. rospy.init_node("control_circle_p")
  19. # 3.创建发布者对象
  20. pub = rospy.Publisher("/turtle1/cmd_vel",Twist,queue_size=1000)
  21. # 4.循环发布运动控制消息
  22. rate = rospy.Rate(10)
  23. msg = Twist()
  24. msg.linear.x = 1.0
  25. msg.linear.y = 0.0
  26. msg.linear.z = 0.0
  27. msg.angular.x = 0.0
  28. msg.angular.y = 0.0
  29. msg.angular.z = 0.5
  30. while not rospy.is_shutdown():
  31. pub.publish(msg)
  32. rate.sleep()

3.运行

首先,启动 roscore; 然后启动乌龟显示节点; 最后执行运动控制节点; 最终执行结果与演示结果类似。

2、实操——话题(topic)订阅(sub)

需求描述:** **已知turtlesim中的乌龟显示节点,会发布当前乌龟的位姿(窗体中乌龟的坐标以及朝向),要求控制乌龟运动,并时时打印当前乌龟的位姿。

结果演示:

实现分析:

  1. 首先,需要启动乌龟显示以及运动控制节点并控制乌龟运动。
  2. 要通过ROS命令,来获取乌龟位姿发布的话题以及消息。
  3. 编写订阅节点,订阅并打印乌龟的位姿。

实现流程:

  1. 通过ros命令获取话题与消息信息。
  2. 编码实现位姿获取节点。
  3. 启动 roscore、turtlesim_node 、控制节点以及位姿订阅节点,控制乌龟运动并输出乌龟的位姿。

1.话题与消息获取

获取话题:/turtle1/pose

  1. rostopic list

获取消息类型:turtlesim/Pose

  1. rostopic type /turtle1/pose

获取消息格式:

  1. rosmsg info turtlesim/Pose

响应结果:

  1. float32 x
  2. float32 y
  3. float32 theta
  4. float32 linear_velocity
  5. float32 angular_velocity

第二章、ROS通信机制 - 图18

2.实现订阅节点

创建功能包需要依赖的功能包: roscpp rospy std_msgs turtlesim

实现方案A: C++

  1. /*
  2. 订阅小乌龟的位姿: 时时获取小乌龟在窗体中的坐标并打印
  3. 准备工作:
  4. 1.获取话题名称 /turtle1/pose
  5. 2.获取消息类型 turtlesim/Pose
  6. 3.运行前启动 turtlesim_node turtle_teleop_key 节点
  7. 实现流程:
  8. 1.包含头文件
  9. 2.初始化 ROS 节点
  10. 3.创建 ROS 句柄
  11. 4.创建订阅者对象
  12. 5.回调函数处理订阅的数据
  13. 6.spin
  14. */
  15. #include "ros/ros.h"
  16. #include "turtlesim/Pose.h"
  17. //ConstPtr&常量指针
  18. void doPose(const turtlesim::Pose::ConstPtr& p){
  19. ROS_INFO("乌龟位姿信息:x=%.2f,y=%.2f,theta=%.2f,lv=%.2f,av=%.2f",
  20. p->x,p->y,p->theta,p->linear_velocity,p->angular_velocity
  21. );
  22. }
  23. int main(int argc, char *argv[])
  24. {
  25. setlocale(LC_ALL,"");
  26. // 2.初始化 ROS 节点
  27. ros::init(argc,argv,"sub_pose");
  28. // 3.创建 ROS 句柄
  29. ros::NodeHandle nh;
  30. // 4.创建订阅者对象
  31. ros::Subscriber sub = nh.subscribe<turtlesim::Pose>("/turtle1/pose",1000,doPose);
  32. // 5.回调函数处理订阅的数据
  33. // 6.spin
  34. ros::spin();
  35. return 0;
  36. }

实现方案B: Python

  1. #! /usr/bin/env python
  2. """
  3. 订阅小乌龟的位姿: 时时获取小乌龟在窗体中的坐标并打印
  4. 准备工作:
  5. 1.获取话题名称 /turtle1/pose
  6. 2.获取消息类型 turtlesim/Pose
  7. 3.运行前启动 turtlesim_node 与 turtle_teleop_key 节点
  8. 实现流程:
  9. 1.导包
  10. 2.初始化 ROS 节点
  11. 3.创建订阅者对象
  12. 4.回调函数处理订阅的数据
  13. 5.spin
  14. """
  15. import rospy
  16. from turtlesim.msg import Pose
  17. def doPose(data):
  18. rospy.loginfo("乌龟坐标:x=%.2f, y=%.2f,theta=%.2f",data.x,data.y,data.theta)
  19. if __name__ == "__main__":
  20. # 2.初始化 ROS 节点
  21. rospy.init_node("sub_pose_p")
  22. # 3.创建订阅者对象
  23. sub = rospy.Subscriber("/turtle1/pose",Pose,doPose,queue_size=1000)
  24. # 4.回调函数处理订阅的数据
  25. # 5.spin
  26. rospy.spin()

乌龟的原点:

第二章、ROS通信机制 - 图19

朝向单给是弧度

3、实操——服务(service)调用(sub)

需求描述:编码实现向 turtlesim 发送请求,在乌龟显示节点的窗体指定位置生成一乌龟,这是一个服务请求操作。

结果演示:

实现分析:

首先,需要启动乌龟显示节点。

要通过ROS命令,来获取乌龟生成服务的服务名称以及服务消息类型。

编写服务请求节点,生成新的乌龟。

实现流程:

通过ros命令获取服务与服务消息信息。

编码实现服务请求节点。

启动 roscore、turtlesim_node 、乌龟生成节点,生成新的乌龟。

1.服务名称与服务消息获取

获取话题:/spawn

  1. rosservice list

第二章、ROS通信机制 - 图20

获取消息类型:turtlesim/Spawn

获取消息格式:

  1. rossrv info turtlesim/Spawn

第二章、ROS通信机制 - 图21

第二章、ROS通信机制 - 图22

响应结果:

  1. float32 x
  2. float32 y
  3. float32 theta
  4. string name
  5. ---
  6. string name

**2.服务客户端实现**

创建功能包需要依赖的功能包: roscpp rospy std_msgs turtlesim

实现方案A:C++

  1. /*
  2. 生成一只小乌龟
  3. 准备工作:
  4. 1.服务话题 /spawn
  5. 2.服务消息类型 turtlesim/Spawn
  6. 3.运行前先启动 turtlesim_node 节点
  7. 实现流程:
  8. 1.包含头文件
  9. 需要包含 turtlesim 包下资源,注意在 package.xml 配置
  10. 2.初始化 ros 节点
  11. 3.创建 ros 句柄
  12. 4.创建 service 客户端
  13. 5.等待服务启动
  14. 6.发送请求
  15. 7.处理响应
  16. */
  17. #include "ros/ros.h"
  18. #include "turtlesim/Spawn.h"
  19. int main(int argc, char *argv[])
  20. {
  21. setlocale(LC_ALL,"");
  22. // 2.初始化 ros 节点
  23. ros::init(argc,argv,"set_turtle");
  24. // 3.创建 ros 句柄
  25. ros::NodeHandle nh;
  26. // 4.创建 service 客户端
  27. ros::ServiceClient client = nh.serviceClient<turtlesim::Spawn>("/spawn");
  28. // 5.等待服务启动
  29. // client.waitForExistence();
  30. ros::service::waitForService("/spawn");
  31. // 6.发送请求
  32. turtlesim::Spawn spawn; //实例化对象
  33. spawn.request.x = 1.0;
  34. spawn.request.y = 1.0;
  35. spawn.request.theta = 1.57;
  36. spawn.request.name = "my_turtle";
  37. bool flag = client.call(spawn); //发送服务请求冰鞋产生bool类型的返回值
  38. // 7.处理响应结果
  39. if (flag)
  40. {
  41. ROS_INFO("新的乌龟生成,名字:%s",spawn.response.name.c_str());
  42. } else {
  43. ROS_INFO("乌龟生成失败!!!");
  44. }
  45. return 0;
  46. }

实现方案B:Python

  1. #! /usr/bin/env python
  2. """
  3. 生成一只小乌龟
  4. 准备工作:
  5. 1.服务话题 /spawn
  6. 2.服务消息类型 turtlesim/Spawn
  7. 3.运行前先启动 turtlesim_node 节点
  8. 实现流程:
  9. 1.导包
  10. 需要包含 turtlesim 包下资源,注意在 package.xml 配置
  11. 2.初始化 ros 节点
  12. 3.创建 service 客户端
  13. 4.等待服务启动
  14. 5.发送请求
  15. 6.处理响应
  16. """
  17. import rospy
  18. from turtlesim.srv import Spawn,SpawnRequest,SpawnResponse
  19. if __name__ == "__main__":
  20. # 2.初始化 ros 节点
  21. rospy.init_node("set_turtle_p")
  22. # 3.创建 service 客户端
  23. client = rospy.ServiceProxy("/spawn",Spawn)
  24. # 4.等待服务启动
  25. client.wait_for_service()
  26. # 5.发送请求
  27. req = SpawnRequest()
  28. req.x = 2.0
  29. req.y = 2.0
  30. req.theta = -1.57
  31. req.name = "my_turtle_p"
  32. try:
  33. response = client.call(req)
  34. # 6.处理响应
  35. rospy.loginfo("乌龟创建成功!,叫:%s",response.name)
  36. except expression as identifier:
  37. rospy.loginfo("服务调用失败")

注意:try/except格式

python中try/except/else/finally语句的完整格式如下所示:
  1. try:
  2. Normal execution block
  3. except A:
  4. Exception A handle
  5. except B:
  6. Exception B handle
  7. except:
  8. Other exception handle
  9. else:
  10. if no exception,get here
  11. finally:
  12. print("finally")
说明: 正常执行的程序在try下面的Normal execution block执行块中执行,在执行过程中如果发生了异常,则 中断当前在Normal execution block中的执行,跳转到对应的异常处理块中开始执行; python 从第一个except X处开始查找,如果找到了对应的exception类型则进入其提供的exception handle中进行处理,如果没有找到则直接进入except块处进行处理。except块是可选项,如果没有提供,该exception将会被提交给python进行默认处理,处理方式则是 终止应用程序并打印提示信息 如果在Normal execution block执行块中执行过程中没有发生任何异常,则在执行完Normal execution block后会进入else执行块中(如果存在的话)执行。 无论是否发生了异常,只要提供了finally语句,以上try/except/else/finally代码块执行的最后一步总是执行finally所对应的代码块。
需要注意的是: 1.在上面所示的完整语句中try/except/else/finally所出现的顺序必须是try—>except X—>except—>else—>finally,即所有的 except必须在else和finally之前 else(如果有的话)必须在finally之前,而 except X必须在except之前。否则会出现语法错误。 2.对于上面所展示的try/except完整格式而言,else和finally都是可选的,而不是必须的,但是如果存在的话e lse必须在finally之前 finally(如果存在的话) 必须在整个语句的最后位置 3.在上面的完整语句中,else语句的存在必须以except X或者except语句为前提, 如果在没有except语句的try block中使用else语句会引发语法错误。也就是说 else不能与try/finally配合使用

4、实操——服务参数(para)设置(sub)

需求描述:** **修改turtlesim乌龟显示节点窗体的背景色,已知背景色是通过参数服务器的方式以 rgb 方式设置的。

结果演示:

实现分析:

  1. 首先,需要启动乌龟显示节点。
  2. 要通过ROS命令,来获取参数服务器中设置背景色的参数。
  3. 编写参数设置节点,修改参数服务器中的参数值。

实现流程:

  1. 通过ros命令获取参数。
  2. 编码实现服参数设置节点。
  3. 启动 roscore、turtlesim_node 与参数设置节点,查看运行结果。

1.参数名获取

获取参数列表:

  1. rosparam list

第二章、ROS通信机制 - 图23

响应结果:

  1. /turtlesim/background_b
  2. /turtlesim/background_g
  3. /turtlesim/background_r

2.参数修改

实现方案A:C++

  1. /*
  2. 注意命名空间的使用。
  3. */
  4. #include "ros/ros.h"
  5. int main(int argc, char *argv[])
  6. {
  7. ros::init(argc,argv,"haha");
  8. ros::NodeHandle nh("turtlesim");
  9. //ros::NodeHandle nh;
  10. // ros::param::set("/turtlesim/background_r",0);
  11. // ros::param::set("/turtlesim/background_g",0);
  12. // ros::param::set("/turtlesim/background_b",0);
  13. nh.setParam("background_r",0);
  14. nh.setParam("background_g",0);
  15. nh.setParam("background_b",0);
  16. return 0;
  17. }

配置文件此处略

实现方案B:Python

  1. #! /usr/bin/env python
  2. import rospy
  3. if __name__ == "__main__":
  4. rospy.init_node("hehe")
  5. # rospy.set_param("/turtlesim/background_r",255)
  6. # rospy.set_param("/turtlesim/background_g",255)
  7. # rospy.set_param("/turtlesim/background_b",255)
  8. rospy.set_param("background_r",255)
  9. rospy.set_param("background_g",255)
  10. rospy.set_param("background_b",255) # 调用时,需要传入 __ns:=xxx

3.运行

首先,启动 roscore; 然后启动背景色设置节点; 最后启动乌龟显示节点; 最终执行结果与演示结果类似。 PS: 注意节点启动顺序,如果先启动乌龟显示节点,后启动背景色设置节点,那么颜色设置不会生效。

4.其他设置方式

方式1:修改小乌龟节点的背景色(命令行实现)

  1. rosparam set /turtlesim/background_b 自定义数值
  1. rosparam set /turtlesim/background_g 自定义数值
  1. rosparam set /turtlesim/background_r 自定义数值
修改相关参数后,重启 turtlesim_node 节点,背景色就会发生改变了

方式2:启动节点时,直接设置参数

  1. rosrun turtlesim turtlesim_node _background_r:=100 _background_g:=0 _background_b:=0

方式3:通过launch文件传参

  1. <launch>
  2. <node pkg="turtlesim" type="turtlesim_node" name="set_bg" output="screen">
  3. <!-- launch 传参策略1 -->
  4. <!-- <param name="background_b" value="0" type="int" />
  5. <param name="background_g" value="0" type="int" />
  6. <param name="background_r" value="0" type="int" /> -->
  7. <!-- launch 传参策略2 -->
  8. <rosparam command="load" file="$(find demo03_test_parameter)/cfg/color.yaml" />
  9. </node>
  10. </launch>

6、通信机制比较

三种通信机制中,参数服务器是一种数据共享机制,可以在不同的节点之间共享数据,话题通信与服务通信是在不同的节点之间传递数据的,三者是ROS中最基础也是应用最为广泛的通信机制。

这其中,话题通信和服务通信有一定的相似性也有本质上的差异,在此将二者做一下简单比较: 二者的实现流程是比较相似的,都是涉及到四个要素:
  • 要素1: 消息的发布方/客户端(Publisher/Client)
  • 要素2: 消息的订阅方/服务端(Subscriber/Server)
  • 要素3: 话题名称(Topic/Service)
  • 要素4: 数据载体(msg/srv)

可以概括为: 两个节点通过话题关联到一起,并使用某种类型的数据载体实现数据传输。

二者的实现也是有本质差异的,具体比较如下:
Topic(话题) Service(服务)
通信模式 发布/订阅 请求、响应
同步性 异步 同步
底层协议 ROSTCP/ROSUDP ROSTCP/ROSUDP
缓冲区 有(队列)
实时性
节点关系 多对多 一对多
通信数据 msg srv
使用场景 连续高频的数据发布与接收:雷达,里程计 偶尔调用或者执行某一项特定功能:拍照、语音识别
不同通信机制有一定的互补性,都有各自适应的应用场景。尤其是话题与服务通信,需要结合具体的应用场景与二者的差异,选择合适的通信机制。

7、本章小结

本章主要介绍了ROS中最基本的也是最核心的通信机制实现: 话题通信服务通信参数服务器。每种通信机制,都介绍了如下内容:
  • 伊始介绍了当前通信机制的应用场景;
  • 介绍了当前通信机制的理论模型;
  • 分别介绍了当前通信机制的C++与Python实现。
除此之外,还介绍了:
  • ROS中的常用命令方便操作、调试节点以及通信信息;
  • 通过实操又将上述知识点加以整合;
  • 最后又着重比较了话题通信与服务通信的相同点以及差异。
掌握本章内容后,基本上就可以从容应对ROS中大部分应用场景了。