查看原文
其他

开发者说丨 ROS理论与实践②:ROS通信机制

赵虚左 Apollo开发者社区 2022-07-29


机器人是一种高度复杂且庞大的系统性实现,在机器人上可能集成各种传感器(雷达、摄像头、GPS等)以及运动控制实现,为了解耦合,在ROS中每一个功能点都是一个单独的进程,每一个进程都是独立运行的。更确切的讲,ROS是进程(也称为Nodes)的分布式框架。 因为这些进程甚至还可分布于不同主机,不同主机协同工作,从而分散计算压力。不过随之也有一个问题:不同的进程是如何通信的?也即不同进程间如何实现数据交换?在此我们就需要介绍一下ROS中的通信机制了。


ROS中的基本通信机制主要有如下三种实现策略:


  • 话题通信(发布订阅模式)

  • 服务通信(请求响应模式)

  • 参数服务器(参数共享模式)


本章的主要内容就是是介绍各个通信机制的应用场景、理论模型、代码实现以及相关操作命令。本章预期达成学习目标如下:


  • 能够熟练介绍ROS中常用的通信机制

  • 能够理解ROS中每种通信机制的理论模型

  • 能够以代码的方式实现各种通信机制对应的案例

  • 能够熟练使用ROS中的一些操作命令

  • 能够独立完成相关实操案例


案例演示:


1.话题演示案例:控制小乌龟做圆周运动



话题演示案例,获取乌龟位姿



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



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



下面是由社区开发者—赵虚左提供的文章,对ROS通信机制进行详细讲解,希望这篇文章能给感兴趣的开发者带来更多帮助。



  ENJOY THE FOLLOWING  







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


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


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


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

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


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


① 概念


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


② 作用


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


③ 案例


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

2.实现对自定义消息的发布与订阅。



话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:


  • ROS Master (管理者)

  • Talker (发布者)

  • Listener (订阅者)


ROS Master负责保管Talker和Listener注册的信息,并匹配话题相同的Talker与Listener,帮助Talker与Listener建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被Listener订阅。



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


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照常通信。



1.发布方


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

<左右滑动以查看完整代码>


2.订阅方




1/*
2需求: 实现基本的话题通信,一方发布数据,一方接收数据,
3     实现的关键点:
4     1.发送方
5     2.接收方
6     3.数据(此处为普通文本)
7
8
9消息订阅方:
10    订阅话题并打印接收到的消息
11
12实现流程:
13    1.包含头文件 
14    2.初始化 ROS 节点:命名(唯一)
15    3.实例化 ROS 句柄
16    4.实例化 订阅者 对象
17    5.处理订阅的消息(回调函数)
18    6.设置循环调用回调函数
19
20*/

21// 1.包含头文件 
22#include "ros/ros.h"
23#include "std_msgs/String.h"
24
25void doMsg(const std_msgs::String::ConstPtr& msg_p){
26ROS_INFO("我听见:%s",msg_p->data.c_str());
27// ROS_INFO("我听见:%s",(*msg_p).data.c_str());
28}
29int main(int argc, char  *argv[])
30
{
31setlocale(LC_ALL,"");
32//2.初始化 ROS 节点:命名(唯一)
33ros::init(argc,argv,"listener");
34//3.实例化 ROS 句柄
35ros::NodeHandle nh;
36
37//4.实例化 订阅者 对象
38ros::Subscriber sub = nh.subscribe("chatter",10,doMsg);
39//5.处理订阅的消息(回调函数)
40
41//     6.设置循环调用回调函数
42ros::spin();//循环读取接收的数据,并调用回调函数处理
43
44return 0;
45}

<左右滑动以查看完整代码>


3.配置 CMakeLists.txt


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

<左右滑动以查看完整代码>


4.执行



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答案如下:

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();延迟第一条数据的发送。



1.发布方

1 #! /usr/bin/env python
2"""
3需求: 实现基本的话题通信,一方发布数据,一方接收数据,
4     实现的关键点:
5     1.发送方
6     2.接收方
7     3.数据(此处为普通文本)
8
9     PS: 二者需要设置相同的话题
10
11
12消息发布方:
13    循环发布信息:HelloWorld 后缀数字编号
14
15实现流程:
16    1.导包 
17    2.初始化 ROS 节点:命名(唯一)
18    3.实例化 发布者 对象
19    4.组织被发布的数据,并编写逻辑发布数据
20
21
22"""

23#1.导包 
24import rospy
25from std_msgs.msg import String
26
27if __name__ == "__main__":
28#2.初始化 ROS 节点:命名(唯一)
29rospy.init_node("talker_p")
30#3.实例化 发布者 对象
31pub = rospy.Publisher("chatter",String,queue_size=10)
32#4.组织被发布的数据,并编写逻辑发布数据
33msg = String()  #创建 msg 对象
34msg_front = "hello 你好"
35count = 0  #计数器 
36# 设置循环频率
37rate = rospy.Rate(1)
38while not rospy.is_shutdown():
39
40    #拼接字符串
41    msg.data = msg_front + str(count)
42
43    pub.publish(msg)
44    rate.sleep()
45    rospy.loginfo("写出的数据:%s",msg.data)
46    count += 1

<左右滑动以查看完整代码>


2.订阅方

1#! /usr/bin/env python
2"""
3需求: 实现基本的话题通信,一方发布数据,一方接收数据,
4     实现的关键点:
5     1.发送方
6     2.接收方
7     3.数据(此处为普通文本)
8
9
10消息订阅方:
11    订阅话题并打印接收到的消息
12
13实现流程:
14    1.导包 
15    2.初始化 ROS 节点:命名(唯一)
16    3.实例化 订阅者 对象
17    4.处理订阅的消息(回调函数)
18    5.设置循环调用回调函数
19
20
21
22"""

23#1.导包 
24import rospy
25from std_msgs.msg import String
26
27def doMsg(msg):
28rospy.loginfo("I heard:%s",msg.data)
29
30if __name__ == "__main__":
31#2.初始化 ROS 节点:命名(唯一)
32rospy.init_node("listener_p")
33#3.实例化 订阅者 对象
34sub = rospy.Subscriber("chatter",String,doMsg,queue_size=10)
35#4.处理订阅的消息(回调函数)
36#5.设置循环调用回调函数
37rospy.spin()

<左右滑动以查看完整代码>


3.添加可执行权限


终端下进入scripts执行:chmod +x *.py


4.配置CMakeLists.txt

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

<左右滑动以查看完整代码>


5.执行




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


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


  • int8, int16, int32, int64 (plus uint*)

  • float32, float64

  • string

  • time, duration

  • other msg files

  • variable-length array[] and fixed-length array[C]


ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。


需求:创建自定义消息格式。


流程:


  1. 按照固定格式创建msg文件。

  2. 编辑配置文件。

  3. 编译生成可以被Python或C++调用的中间文件。


1.定义msg文件


功能包下新建msg目录,添加文件Person.msg。

1string name
2uint16 age
3float64 height

<左右滑动以查看完整代码>


2.编辑配置文件


package.xml中添加编译依赖与执行依赖。

 

1 message_generationbuild_depend>
2 message_runtimeexec_depend>

<左右滑动以查看完整代码>


CMakeLists.txt编辑msg相关配置


1find_package(catkin REQUIRED COMPONENTS
2roscpp
3rospy
4std_msgs
5message_generation
6)
7# 需要加入 message_generation,必须有 std_msgs
8## 配置 msg 源文件
9add_message_files(
10FILES
11Person.msg
12)
13# 生成消息时依赖于 std_msgs
14generate_messages(
15DEPENDENCIES
16std_msgs
17)
18#执行时依赖
19catkin_package(
20#  INCLUDE_DIRS include
21#  LIBRARIES demo02_talker_listener
22CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
23#  DEPENDS system_lib
24)

<左右滑动以查看完整代码>


3.编译


编译后的中间文件查看:


C++需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)



Python需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/msg)



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



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}

<左右滑动以查看完整代码>


1.发布方

1/*
2需求: 循环发布人的信息
3
4*/

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

<左右滑动以查看完整代码>


2.订阅方

1/*
2需求: 订阅人的信息
3
4*/

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

<左右滑动以查看完整代码>


3.配置CMakeLists.txt


需要添加add_dependencies用以设置所依赖的消息相关的中间文件。

1add_executable(person_talker src/person_talker.cpp)
2add_executable(person_listener src/person_listener.cpp)
3
4
5
6add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp)
7add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp)
8
9
10target_link_libraries(person_talker
11${catkin_LIBRARIES}
12)
13target_link_libraries(person_listener
14${catkin_LIBRARIES}
15)

<左右滑动以查看完整代码>


4.执行


执行方式同之前。



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"""

7import rospy
8from demo02_talker_listener.msg import Person
9
10
11if __name__ == "__main__":
12#1.初始化 ROS 节点
13rospy.init_node("talker_person_p")
14#2.创建发布者对象
15pub = rospy.Publisher("chatter_person",Person,queue_size=10)
16#3.组织消息
17p = Person()
18p.name = "葫芦瓦"
19p.age = 18
20p.height = 0.75
21
22#4.编写消息发布逻辑
23rate = rospy.Rate(1)
24while not rospy.is_shutdown():
25    pub.publish(p)  #发布消息
26    rate.sleep()  #休眠
27    rospy.loginfo("姓名:%s, 年龄:%d, 身高:%.2f",p.name, p.age, p.height)

<左右滑动以查看完整代码>


2.订阅方

1#! /usr/bin/env python
2"""
3订阅方:
4    订阅消息
5
6"""

7import rospy
8from demo02_talker_listener.msg import Person
9
10def doPerson(p):
11rospy.loginfo("接收到的人的信息:%s, %d, %.2f",p.name, p.age, p.height)
12
13
14if __name__ == "__main__":
15#1.初始化节点
16rospy.init_node("listener_person_p")
17#2.创建订阅者对象
18sub = rospy.Subscriber("chatter_person",Person,doPerson,queue_size=10)
19rospy.spin() #4.循环

<左右滑动以查看完整代码>


3.权限设置


终端下进入scripts执行:chmod +x *.py。


4.配置CMakeLists.txt

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

<左右滑动以查看完整代码>


5.执行


执行方式同之前。




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


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


在上述场景中,就使用到了服务通信。


  • 一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果。


与上述应用类似的,服务通信更适用于对实时性有要求、具有一定逻辑处理的应用场景。


① 概念


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


② 作用


用于不断更新的、有一定逻辑处理需求的数据传输场景。


③ 案例


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



服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:


  • ROS master(管理者)

  • Server(服务端)

  • Client(客户端)


ROS Master负责保管Server和Client注册的信息,并匹配话题相同的Server与Client ,帮助Server与Client建立连接,连接建立后,Client发送请求信息,Server返回响应信息。



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


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。


注意:步骤2ROS Master向Client发送的是TCP地址信息。



srv文件内的可用数据类型与msg文件一致,且定义srv实现流程与自定义msg实现流程类似:

  • 按照固定格式创建srv文件。

  • 编辑配置文件。

  • 编译生成中间文件。


1.定义srv文件


服务通信中,数据分成两部分,请求与响应,在srv文件中请求和响应使用---分割,具体实现如下:


功能包下新建srv目录,添加xxx.srv文件,内容:

1# 客户端请求时发送的两个数字
2int32 num1
3int32 num2
4---

5# 服务器响应发送的数据
6int32 sum

<左右滑动以查看完整代码>


2.编辑配置文件


package.xml中添加编译依赖与执行依赖

 

1message_generationbuild_depend>
2message_runtimeexec_depend>

<左右滑动以查看完整代码>


CMakeLists.txt编辑srv相关配置

1find_package(catkin REQUIRED COMPONENTS
2roscpp
3rospy
4std_msgs
5message_generation
6)
7# 需要加入 message_generation,必须有 std_msgs
8add_service_files(
9FILES
10AddInts.srv
11)
12generate_messages(
13DEPENDENCIES
14std_msgs
15)

<左右滑动以查看完整代码>


注意:官网没有在catkin_package中配置message_runtime,经测试配置也可以。


3.编译


编译后的中间文件查看:


C++ 需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)。



Python需要调用的中间文件(../工作空间/devel/lib/python3/dist-packages/包名/msg)。



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



0.vscode配置


需要像之前自定义msg实现一样配置c_cpp_properies.json文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:

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}

<左右滑动以查看完整代码>


1.服务端

1/*
2需求: 
3    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
4    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
5    客户端再解析
6
7服务器实现:
8    1.包含头文件
9    2.初始化 ROS 节点
10    3.创建 ROS 句柄
11    4.创建 服务 对象
12    5.回调函数处理请求并产生响应
13    6.由于请求有多个,需要调用 ros::spin()
14
15*/

16#include "ros/ros.h"
17#include "demo03_server_client/AddInts.h"
18
19// bool 返回值由于标志是否处理成功
20bool doReq(demo03_server_client::AddInts::Request& req,
21      demo03_server_client::AddInts::Response& resp)
{
22int num1 = req.num1;
23int num2 = req.num2;
24
25ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
26
27//逻辑处理
28if (num1 < 0 || num2 < 0)
29{
30    ROS_ERROR("提交的数据异常:数据不可以为负数");
31    return false;
32}
33
34//如果没有异常,那么相加并将结果赋值给 resp
35resp.sum = num1 + num2;
36return true;
37
38
39}
40
41int main(int argc, char *argv[])
42
{
43setlocale(LC_ALL,"");
44// 2.初始化 ROS 节点
45ros::init(argc,argv,"AddInts_Server");
46// 3.创建 ROS 句柄
47ros::NodeHandle nh;
48// 4.创建 服务 对象
49ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
50ROS_INFO("服务已经启动....");
51//     5.回调函数处理请求并产生响应
52//     6.由于请求有多个,需要调用 ros::spin()
53ros::spin();
54return 0;
55}

<左右滑动以查看完整代码>


2.客户端

1  /*
2需求: 
3    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
4    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
5    客户端再解析
6
7服务器实现:
8    1.包含头文件
9    2.初始化 ROS 节点
10    3.创建 ROS 句柄
11    4.创建 服务 对象
12    5.回调函数处理请求并产生响应
13    6.由于请求有多个,需要调用 ros::spin()
14
15*/

16#include "ros/ros.h"
17#include "demo03_server_client/AddInts.h"
18
19// bool 返回值由于标志是否处理成功
20bool doReq(demo03_server_client::AddInts::Request& req,
21      demo03_server_client::AddInts::Response& resp)
{
22int num1 = req.num1;
23int num2 = req.num2;
24
25ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
26
27//逻辑处理
28if (num1 < 0 || num2 < 0)
29{
30    ROS_ERROR("提交的数据异常:数据不可以为负数");
31    return false;
32}
33
34//如果没有异常,那么相加并将结果赋值给 resp
35resp.sum = num1 + num2;
36return true;
37
38
39}
40
41/*
42需求: 
43    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
44    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
45    客户端再解析
46
47服务器实现:
48    1.包含头文件
49    2.初始化 ROS 节点
50    3.创建 ROS 句柄
51    4.创建 客户端 对象
52    5.请求服务,接收响应
53
54*/

55// 1.包含头文件
56#include "ros/ros.h"
57#include "demo03_server_client/AddInts.h"
58
59int main(int argc, char *argv[])
60
{
61setlocale(LC_ALL,"");
62
63// 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
64if (argc != 3)
65// if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
66{
67    ROS_ERROR("请提交两个整数");
68    return 1;
69}
70
71
72// 2.初始化 ROS 节点
73ros::init(argc,argv,"AddInts_Client");
74// 3.创建 ROS 句柄
75ros::NodeHandle nh;
76// 4.创建 客户端 对象
77ros::ServiceClient client = nh.serviceClient("AddInts");
78//等待服务启动成功
79//方式1
80ros::service::waitForService("AddInts");
81//方式2
82// client.waitForExistence();
83// 5.组织请求数据
84demo03_server_client::AddInts ai;
85ai.request.num1 = atoi(argv[1]);
86ai.request.num2 = atoi(argv[2]);
87// 6.发送请求,返回 bool 值,标记是否成功
88bool flag = client.call(ai);
89// 7.处理响应
90if (flag)
91{
92    ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
93}
94else
95{
96    ROS_ERROR("请求处理失败....");
97    return 1;
98}
99
100return 0;
101}

<左右滑动以查看完整代码>


3.配置CMakeLists.txt

1add_executable(AddInts_Server src/AddInts_Server.cpp)
2add_executable(AddInts_Client src/AddInts_Client.cpp)
3
4
5add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
6add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)
7
8
9target_link_libraries(AddInts_Server
10${catkin_LIBRARIES}
11)
12target_link_libraries(AddInts_Client
13${catkin_LIBRARIES}
14)

<左右滑动以查看完整代码>


4.执行


流程:


  • 需要先启动服务:rosrun包名服务。

  • 然后再调用客户端:rosrun包名客户端参数1参数2。


结果:

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


注意:

如果先启动客户端,那么会导致运行失败。


优化:

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

或:ros::service::waitForService("AddInts");

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

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


0.vscode配置


需要像之前自定义msg实现一样配置settings.json文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:

1{
2"python.autoComplete.extraPaths": [
3    "/opt/ros/noetic/lib/python3/dist-packages",
4]
5}

<左右滑动以查看完整代码>


1.服务端

1#! /usr/bin/env python
2"""
3需求: 
4    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
5    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
6    客户端再解析
7
8服务器端实现:
9    1.导包
10    2.初始化 ROS 节点
11    3.创建服务对象
12    4.回调函数处理请求并产生响应
13    5.spin 函数
14
15"""

16# 1.导包
17import rospy
18from demo03_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse
19# 回调函数的参数是请求对象,返回值是响应对象
20def doReq(req):
21# 解析提交的数据
22sum = req.num1 + req.num2
23rospy.loginfo("提交的数据:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum)
24
25# 创建响应对象,赋值并返回
26# resp = AddIntsResponse()
27# resp.sum = sum
28resp = AddIntsResponse(sum)
29return resp
30
31
32if __name__ == "__main__":
33# 2.初始化 ROS 节点
34rospy.init_node("addints_server_p")
35# 3.创建服务对象
36server = rospy.Service("AddInts",AddInts,doReq)
37# 4.回调函数处理请求并产生响应
38# 5.spin 函数
39rospy.spin()

<左右滑动以查看完整代码>


2.客户端

1#! /usr/bin/env python
2
3"""
4需求: 
5    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
6    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
7    客户端再解析
8
9客户端实现:
10    1.导包
11    2.初始化 ROS 节点
12    3.创建请求对象
13    4.发送请求
14    5.接收并处理响应
15
16优化:
17    加入数据的动态获取
18
19
20"""

21#1.导包
22import rospy
23from demo03_server_client.srv import *
24import sys
25
26if __name__ == "__main__":
27
28#优化实现
29if len(sys.argv) != 3:
30    rospy.logerr("请正确提交参数")
31    sys.exit(1)
32
33
34# 2.初始化 ROS 节点
35rospy.init_node("AddInts_Client_p")
36# 3.创建请求对象
37client = rospy.ServiceProxy("AddInts",AddInts)
38# 请求前,等待服务已经就绪
39# 方式1:
40# rospy.wait_for_service("AddInts")
41# 方式2
42client.wait_for_service()
43# 4.发送请求,接收并处理响应
44# 方式1
45# resp = client(3,4)
46# 方式2
47# resp = client(AddIntsRequest(1,5))
48# 方式3
49req = AddIntsRequest()
50# req.num1 = 100
51# req.num2 = 200 
52
53#优化
54req.num1 = int(sys.argv[1])
55req.num2 = int(sys.argv[2]) 
56
57resp = client.call(req)
58rospy.loginfo("响应结果:%d",resp.sum)

<左右滑动以查看完整代码>


3.设置权限


终端下进入scripts执行:chmod +x *.py。


4.配置 CMakeLists.txt

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

<左右滑动以查看完整代码>

5.执行


流程:


  • 需要先启动服务:rosrun 包名 服务。

  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2。


结果:

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




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


导航实现时,会进行路径规划,比如:全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径


上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:


  • 路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数


参数服务器,一般适用于存在数据共享的一些应用场景。


① 概念


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


② 作用


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


③ 案例


实现参数增删改查操作。



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


  • ROS Master (管理者)

  • Talker (参数设置者)

  • Listener (参数调用者)


ROS Master作为一个公共容器保存参数,Talker可以向容器中设置参数,Listener可以获取参数。



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


1.Talker设置参数


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


2.Listene 获取参数


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


3.ROS Master向Listener发送参数值


ROS Master根据步骤2请求提供的参数名查找参数值,并将查询结果通过RPC发送给Listener。


参数可使用数据类型:


  • 32-bit integers

  • booleans

  • strings

  • doubles

  • iso8601 dates

  • lists

  • base64-encoded binary data

  • 字典


注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的而二进制的简单数据。



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


在C++中实现参数服务器数据的增删改查,可以通过两套API实现:


  • ros::NodeHandle

  • ros::param


下面为具体操作演示


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

1/*
2参数服务器操作之新增与修改(二者API一样)_C++实现:
3在 roscpp 中提供了两套 API 实现参数操作
4ros::NodeHandle
5    setParam("键",值)
6ros::param
7    set("键","值")
8
9示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数
10    修改(相同的键,不同的值)
11
12*/

13#include "ros/ros.h"
14
15int main(int argc, char *argv[])
16
{
17ros::init(argc,argv,"set_update_param");
18
19std::vector<std::string> stus;
20stus.push_back("zhangsan");
21stus.push_back("李四");
22stus.push_back("王五");
23stus.push_back("孙大脑袋");
24
25std::map<std::string,std::string> friends;
26friends["guo"] = "huang";
27friends["yuang"] = "xiao";
28
29//NodeHandle--------------------------------------------------------
30ros::NodeHandle nh;
31nh.setParam("nh_int",10); //整型
32nh.setParam("nh_double",3.14); //浮点型
33nh.setParam("nh_bool",true); //bool
34nh.setParam("nh_string","hello NodeHandle"); //字符串
35nh.setParam("nh_vector",stus); // vector
36nh.setParam("nh_map",friends); // map
37
38//修改演示(相同的键,不同的值)
39nh.setParam("nh_int",10000);
40
41//param--------------------------------------------------------
42ros::param::set("param_int",20);
43ros::param::set("param_double",3.14);
44ros::param::set("param_string","Hello Param");
45ros::param::set("param_bool",false);
46ros::param::set("param_vector",stus);
47ros::param::set("param_map",friends);
48
49//修改演示(相同的键,不同的值)
50ros::param::set("param_int",20000);
51
52return 0;
53}

<左右滑动以查看完整代码>


2.参数服务器获取参数

1/*
2参数服务器操作之查询_C++实现:
3在 roscpp 中提供了两套 API 实现参数操作
4ros::NodeHandle
5
6    param(键,默认值) 
7        存在,返回对应结果,否则返回默认值
8
9    getParam(键,存储结果的变量)
10        存在,返回 true,且将值赋值给参数2
11        若果键不存在,那么返回值为 false,且不为参数2赋值
12
13    getParamCached键,存储结果的变量)--提高变量获取效率
14        存在,返回 true,且将值赋值给参数2
15        若果键不存在,那么返回值为 false,且不为参数2赋值
16
17    getParamNames(std::vector)
18        获取所有的键,并存储在参数 vector 中 
19
20    hasParam(键)
21        是否包含某个键,存在返回 true,否则返回 false
22
23    searchParam(参数1,参数2)
24        搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
25
26ros::param ----- 与 NodeHandle 类似
27
28
29
30
31
32*/

33
34#include "ros/ros.h"
35
36int main(int argc, char *argv[])
37
{
38setlocale(LC_ALL,"");
39ros::init(argc,argv,"get_param");
40
41//NodeHandle--------------------------------------------------------
42/*
43ros::NodeHandle nh;
44// param 函数
45int res1 = nh.param("nh_int",100); // 键存在
46int res2 = nh.param("nh_int2",100); // 键不存在
47ROS_INFO("param获取结果:%d,%d",res1,res2);
48
49// getParam 函数
50int nh_int_value;
51double nh_double_value;
52bool nh_bool_value;
53std::string nh_string_value;
54std::vector stus;
55std::map friends;
56
57nh.getParam("nh_int",nh_int_value);
58nh.getParam("nh_double",nh_double_value);
59nh.getParam("nh_bool",nh_bool_value);
60nh.getParam("nh_string",nh_string_value);
61nh.getParam("nh_vector",stus);
62nh.getParam("nh_map",friends);
63
64ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
65        nh_int_value,
66        nh_double_value,
67        nh_string_value.c_str(),
68        nh_bool_value
69        );
70for (auto &&stu : stus)
71{
72    ROS_INFO("stus 元素:%s",stu.c_str());        
73}
74
75for (auto &&f : friends)
76{
77    ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
78}
79
80// getParamCached()
81nh.getParamCached("nh_int",nh_int_value);
82ROS_INFO("通过缓存获取数据:%d",nh_int_value);
83
84//getParamNames()
85std::vector param_names1;
86nh.getParamNames(param_names1);
87for (auto &&name : param_names1)
88{
89    ROS_INFO("名称解析name = %s",name.c_str());        
90}
91ROS_INFO("----------------------------");
92
93ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int"));
94ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt"));
95
96std::string key;
97nh.searchParam("nh_int",key);
98ROS_INFO("搜索键:%s",key.c_str());
99*/

100//param--------------------------------------------------------
101ROS_INFO("++++++++++++++++++++++++++++++++++++++++");
102int res3 = ros::param::param("param_int",20); //存在
103int res4 = ros::param::param("param_int2",20); // 不存在返回默认
104ROS_INFO("param获取结果:%d,%d",res3,res4);
105
106// getParam 函数
107int param_int_value;
108double param_double_value;
109bool param_bool_value;
110std::string param_string_value;
111std::vector<std::string> param_stus;
112std::map<std::stringstd::string> param_friends;
113
114ros::param::get("param_int",param_int_value);
115ros::param::get("param_double",param_double_value);
116ros::param::get("param_bool",param_bool_value);
117ros::param::get("param_string",param_string_value);
118ros::param::get("param_vector",param_stus);
119ros::param::get("param_map",param_friends);
120
121ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
122        param_int_value,
123        param_double_value,
124        param_string_value.c_str(),
125        param_bool_value
126        );
127for (auto &&stu : param_stus)
128{
129    ROS_INFO("stus 元素:%s",stu.c_str());        
130}
131
132for (auto &&f : param_friends)
133{
134    ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
135}
136
137// getParamCached()
138ros::param::getCached("param_int",param_int_value);
139ROS_INFO("通过缓存获取数据:%d",param_int_value);
140
141//getParamNames()
142std::vector<std::string> param_names2;
143ros::param::getParamNames(param_names2);
144for (auto &&name : param_names2)
145{
146    ROS_INFO("名称解析name = %s",name.c_str());        
147}
148ROS_INFO("----------------------------");
149
150ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int"));
151ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));
152
153std::string key;
154ros::param::search("param_int",key);
155ROS_INFO("搜索键:%s",key.c_str());
156
157return 0;
158}

<左右滑动以查看完整代码>


3.参数服务器删除参数

1/* 
2参数服务器操作之删除_C++实现:
3
4ros::NodeHandle
5    deleteParam("键")
6    根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
7
8ros::param
9    del("键")
10    根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
11
12
13*/

14#include "ros/ros.h"
15
16
17int main(int argc, char *argv[])
18
{   
19setlocale(LC_ALL,"");
20ros::init(argc,argv,"delete_param");
21
22ros::NodeHandle nh;
23bool r1 = nh.deleteParam("nh_int");
24ROS_INFO("nh 删除结果:%d",r1);
25
26bool r2 = ros::param::del("param_int");
27ROS_INFO("param 删除结果:%d",r2);
28
29return 0;
30}

<左右滑动以查看完整代码>



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


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


1#! /usr/bin/env python
2"""
3参数服务器操作之新增与修改(二者API一样)_Python实现:
4"""

5
6import rospy
7
8if __name__ == "__main__":
9rospy.init_node("set_update_paramter_p")
10
11# 设置各种类型参数
12rospy.set_param("p_int",10)
13rospy.set_param("p_double",3.14)
14rospy.set_param("p_bool",True)
15rospy.set_param("p_string","hello python")
16rospy.set_param("p_list",["hello","haha","xixi"])
17rospy.set_param("p_dict",{"name":"hulu","age":8})
18
19# 修改
20rospy.set_param("p_int",100)

<左右滑动以查看完整代码>


2.参数服务器获取参数

1#! /usr/bin/env python
2
3"""
4参数服务器操作之查询_Python实现:    
5    get_param(键,默认值)
6        当键存在时,返回对应的值,如果不存在返回默认值
7    get_param_cached
8    get_param_names
9    has_param
10    search_param
11"""

12
13import rospy
14
15if __name__ == "__main__":
16rospy.init_node("get_param_p")
17
18#获取参数
19int_value = rospy.get_param("p_int",10000)
20double_value = rospy.get_param("p_double")
21bool_value = rospy.get_param("p_bool")
22string_value = rospy.get_param("p_string")
23p_list = rospy.get_param("p_list")
24p_dict = rospy.get_param("p_dict")
25
26rospy.loginfo("获取的数据:%d,%.2f,%d,%s",
27            int_value,
28            double_value,
29            bool_value,
30            string_value)
31for ele in p_list:
32    rospy.loginfo("ele = %s", ele)
33
34rospy.loginfo("name = %s, age = %d",p_dict["name"],p_dict["age"])
35
36# get_param_cached
37int_cached = rospy.get_param_cached("p_int")
38rospy.loginfo("缓存数据:%d",int_cached)
39
40# get_param_names
41names = rospy.get_param_names()
42for name in names:
43    rospy.loginfo("name = %s",name)
44
45rospy.loginfo("-"*80)
46
47# has_param
48flag = rospy.has_param("p_int")
49rospy.loginfo("包含p_int吗?%d",flag)
50
51# search_param
52key = rospy.search_param("p_int")
53rospy.loginfo("搜索的键 = %s",key)

<左右滑动以查看完整代码>


3.参数服务器删除参数

1#! /usr/bin/env python
2"""
3参数服务器操作之删除_Python实现:
4rospy.delete_param("键")
5键存在时,可以删除成功,键不存在时,会抛出异常
6"""

7import rospy
8
9if __name__ == "__main__":
10rospy.init_node("delete_param_p")
11
12try:
13    rospy.delete_param("p_int")
14except Exception as e:
15    rospy.loginfo("删除失败")

<左右滑动以查看完整代码>




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


在ROS同提供了一些实用的命令行工具,可以用于获取不同节点的各类信息,常用的命令如下:


  • rosnode :操作节点

  • rostopic : 操作话题

  • rosservice:操作服务

  • rosmsg:操作msg消息

  • rossrv:操作srv消息

  • rosparam:操作参数


作用


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



rosnode是用于获取节点信息的命令。

1rosnode ping    测试到节点的连接状态
2rosnode list    列出活动节点
3rosnode info    打印节点信息
4rosnode machine    列出指定设备上节点
5rosnode kill    杀死某个节点
6rosnode cleanup    清除不可连接的节点

<左右滑动以查看完整代码>


  • rosnode cleanup应用


启动乌龟节点,然后ctrl+c关闭,该节点并没被彻底清除,可以使用cleanup清除节点。



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

1rostopic bw     显示主题使用的带宽
2rostopic delay  显示带有 header 的主题延迟
3rostopic echo   打印消息到屏幕
4rostopic find   根据类型查找主题
5rostopic hz     显示主题的发布频率
6rostopic info   显示主题相关信息
7rostopic list   显示所有活动状态下的主题
8rostopic pub    将数据发布到主题
9rostopic type   打印主题类型

<左右滑动以查看完整代码>


  • rostopic list(-v)


直接调用即可,控制台将打印当前运行状态下的主题名称。

rostopic list -v :获取话题详情(比如列出:发布者和订阅者个数...)


  • rostopic pub


可以直接调用命令向订阅者发布消息。

为roboware自动生成的发布/订阅模型案例中的订阅者发布一条字符串。

1rostopic pub /主题名称 消息类型 消息内容
2rostopic 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
12rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist
13"linear:
14x: 1.0
15y: 0.0
16z: 0.0
17angular:
18x: 0.0
19y: 0.0
20z: 2.0"

21// 以 10HZ 的频率循环发送运动信息

<左右滑动以查看完整代码>


  • rostpic echo


获取指定话题当前发布的消息


  • rostopic info


获取当前话题的小关信息


消息类型


发布者信息


订阅者信息


  • rostopic type


列出话题的消息类型


  • rostopic find 消息类型


根据消息类型查找话题


  • rostopic delay


列出消息头信息


  • rostopic hz


列出消息发布频率


  • rostopic bw


列出消息发布带宽



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

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


1rosservice args 打印服务参数
2rosservice call    使用提供的参数调用服务
3rosservice find    按照服务类型查找服务
4rosservice info    打印有关服务的信息
5rosservice list    列出所有活动的服务
6rosservice type    打印服务类型
7rosservice 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


打印服务参数

1rosservice args /spawn
2x y theta name

<左右滑动以查看完整代码>


  • rosservice call


调用服务


为小乌龟的案例生成一只新的乌龟

1rosservice call /spawn "x: 1.0
2y: 2.0
3theta: 0.0
4name: 'xxx'"

5name"xxx"
6
7//生成一只叫 xxx 的乌龟

<左右滑动以查看完整代码>


  • rosservice find


根据消息类型获取话题


  • rosservice info


获取服务话题详情


  • rosservice type


获取消息类型


  • rosservice uri


获取服务器uri


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


rosmsg演示

1rosmsg show    显示消息描述
2rosmsg info    显示消息信息
3rosmsg list    列出所有消息
4rosmsg md5    显示 md5 加密后的消息
5rosmsg package    显示某个功能包下的所有消息
6rosmsg packages    列出包含消息的功能包

<左右滑动以查看完整代码>


  • rosmsg list


会列出当前ROS中的所有msg


  • rosmsg packages


列出包含消息的所有包


  • rosmsg package


列出某个包下的所有msg

1//rosmsg package 包名 
2rosmsg package turtlesim 

<左右滑动以查看完整代码>


  • rosmsg show


显示消息描述

1//rosmsg show 消息名称
2rosmsg show turtlesim/Pose
3结果:
4float32 x
5float32 y
6float32 theta
7float32 linear_velocity
8float32 angular_velocity

<左右滑动以查看完整代码>


  • rosmsg info


作用与rosmsg show一样


  • rosmsg md5

    (资料:

    http://wiki.ros.org/ROS/Technical%20Overview#Message_serialization_and_msg_MD5_sums


一种校验算法,保证数据传输的一致性。



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


1rossrv show    显示服务消息详情
2rossrv info    显示服务消息相关信息
3rossrv list    列出所有服务信息
4rossrv md5    显示 md5 加密后的服务消息
5rossrv package    显示某个包下所有服务消息
6rossrv packages    显示包含服务消息的所有包

<左右滑动以查看完整代码>

  • rossrv list


会列出当前ROS中的所有srv消息


  • rossrv packages


列出包含服务消息的所有包


  • rossrv package


列出某个包下的所有msg


1//rossrv package 包名 
2rossrv package turtlesim

<左右滑动以查看完整代码>


  • rossrv show


显示消息描述


1//rossrv show 消息名称
2rossrv show turtlesim/Spawn
3结果:
4float32 x
5float32 y
6float32 theta
7string name
8---
9string name

<左右滑动以查看完整代码>


  • rossrv info


作用与rossrv show一致


  • rossrv md5

对service数据使用md5校验(加密)

rosparam包含rosparam命令行工具,用于使用YAML编码文件在参数服务器上获取和设置ROS参数。

1rosparam set    设置参数
2rosparam get    获取参数
3rosparam load    从外部文件加载参数
4rosparam dump    将参数写出到外部文件
5rosparam delete    删除参数
6rosparam list    列出所有参数

<左右滑动以查看完整代码>


  • rosparam list列出所有参数


1rosparam list
2
3//默认结果
4/rosdistro
5/roslaunch/uris/host_helloros_virtual_machine__42911
6/rosversion
7/run_id

<左右滑动以查看完整代码>


  • rosparam set设置参数

1rosparam set name huluwa
2
3//再次调用 rosparam list 结果
4/name
5/rosdistro
6/roslaunch/uris/host_helloros_virtual_machine__42911
7/rosversion
8/run_id

<左右滑动以查看完整代码>




  • rosparam get获取参数

1rosparam get name
2
3//结果
4huluwa

<左右滑动以查看完整代码>


  • rosparam delete删除参数

1rosparam delete name
2
3//结果
4//去除了name

<左右滑动以查看完整代码>




  • rosparam load(先准备yaml文件)从外部文件加载参数

rosparam load xxx.yaml

<左右滑动以查看完整代码>


  • rosparam dump将参数写出到外部文件


rosparam dump yyy.yaml

<左右滑动以查看完整代码>






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


熟悉、强化通信模式应用。


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


结果演示:



实现分析:


  • 乌龟运动控制实现,关键节点有两个,一个是乌龟运动显示节点 turtlesim_node,另一个是控制节点,二者是订阅发布模式实现通信的,乌龟运动显示节点直接调用即可,运动控制节点之前是使用的turtle_teleop_key通过键盘 控制,现在需要自定义控制节点。

  • 控制节点自实现时,首先需要了解控制节点与显示节点通信使用的话题与消息,可以使用ros命令结合计算图来获取。

  • 了解了话题与消息之后,通过C++或Python编写运动控制节点,通过指定的话题,按照一定的逻辑发布消息即可。


实现流程:


1.通过ros命令获取话题与消息信息。

2.编码实现运动控制节点。

3.启动roscore、turtlesim_node以及自定义的控制节点,查看运行结果。


1.话题与消息获取


获取话题:


1/turtle1/cmd_vel
2rostopic list

<左右滑动以查看完整代码>




获取消息类型:


1geometry_msgs/Twist
2rostopic type /turtle1/cmd_vel

<左右滑动以查看完整代码>

获取消息格式:


rosmsg info geometry_msgs/Twist

<左右滑动以查看完整代码>


响应结果:


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

<左右滑动以查看完整代码>


2.实现发布节点


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


实现方案A:C++

1/*
2编写 ROS 节点,控制小乌龟画圆
3
4准备工作:
5    1.获取topic(已知: /turtle1/cmd_vel)
6    2.获取消息类型(已知: geometry_msgs/Twist)
7    3.运行前,注意先启动 turtlesim_node 节点
8
9实现流程:
10    1.包含头文件
11    2.初始化 ROS 节点
12    3.创建发布者对象
13    4.循环发布运动控制消息
14*/

15
16#include "ros/ros.h"
17#include "geometry_msgs/Twist.h"
18
19int main(int argc, char *argv[])
20
{
21setlocale(LC_ALL,"");
22// 2.初始化 ROS 节点
23ros::init(argc,argv,"control");
24ros::NodeHandle nh;
25// 3.创建发布者对象
26ros::Publisher pub = nh.advertise("/turtle1/cmd_vel",1000);
27// 4.循环发布运动控制消息
28//4-1.组织消息
29geometry_msgs::Twist msg;
30msg.linear.x = 1.0;
31msg.linear.y = 0.0;
32msg.linear.z = 0.0;
33
34msg.angular.x = 0.0;
35msg.angular.y = 0.0;
36msg.angular.z = 2.0;
37
38//4-2.设置发送频率
39ros::Rate r(10);
40//4-3.循环发送
41while (ros::ok())
42{
43    pub.publish(msg);
44
45    ros::spinOnce();
46}
47
48
49return 0;
50}

<左右滑动以查看完整代码>


配置文件此处略。


实现方案B:Python

1#! /usr/bin/env python
2"""
3编写 ROS 节点,控制小乌龟画圆
4
5准备工作:
6    1.获取topic(已知: /turtle1/cmd_vel)
7    2.获取消息类型(已知: geometry_msgs/Twist)
8    3.运行前,注意先启动 turtlesim_node 节点
9
10实现流程:
11    1.导包
12    2.初始化 ROS 节点
13    3.创建发布者对象
14    4.循环发布运动控制消息
15
16"""

17
18import rospy
19from geometry_msgs.msg import Twist
20
21if __name__ == "__main__":
22# 2.初始化 ROS 节点
23rospy.init_node("control_circle_p")
24# 3.创建发布者对象
25pub = rospy.Publisher("/turtle1/cmd_vel",Twist,queue_size=1000)
26# 4.循环发布运动控制消息
27rate = rospy.Rate(10)
28msg = Twist()
29msg.linear.x = 1.0
30msg.linear.y = 0.0
31msg.linear.z = 0.0
32msg.angular.x = 0.0
33msg.angular.y = 0.0
34msg.angular.z = 0.5
35
36while not rospy.is_shutdown():
37    pub.publish(msg)
38    rate.sleep()

<左右滑动以查看完整代码>


权限设置以及配置文件此处略。


3.运行


首先,启动roscore;

然后启动乌龟显示节点;

最后执行运动控制节点;

最终执行结果与演示结果类似。



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


结果演示:



实现分析:


  • 首先,需要启动乌龟显示以及运动控制节点并控制乌龟运动。

  • 要通过ROS命令,来获取乌龟位姿发布的话题以及消息。

  • 编写订阅节点,订阅并打印乌龟的位姿。


实现流程:


1.通过ros命令获取话题与消息信息。

2.编码实现位姿获取节点。

3.启动roscore、turtlesim_node 、控制节点以及位姿订阅节点,控制乌龟运动并输出乌龟的位姿。


1.话题与消息获取


获取话题:

1/turtle1/pose
2rostopic list

<左右滑动以查看完整代码>


获取消息类型:

1turtlesim/Pose
2rostopic type /turtle1/pose

<左右滑动以查看完整代码>


获取消息格式:


rosmsg info turtlesim/Pose

<左右滑动以查看完整代码>


响应结果:


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

<左右滑动以查看完整代码>


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实现流程:
9    1.包含头文件
10    2.初始化 ROS 节点
11    3.创建 ROS 句柄
12    4.创建订阅者对象
13    5.回调函数处理订阅的数据
14    6.spin
15*/

16
17#include "ros/ros.h"
18#include "turtlesim/Pose.h"
19
20void doPose(const turtlesim::Pose::ConstPtr& p){
21ROS_INFO("乌龟位姿信息:x=%.2f,y=%.2f,theta=%.2f,lv=%.2f,av=%.2f",
22    p->x,p->y,p->theta,p->linear_velocity,p->angular_velocity
23);
24}
25
26int main(int argc, char *argv[])
27
{
28setlocale(LC_ALL,"");
29// 2.初始化 ROS 节点
30ros::init(argc,argv,"sub_pose");
31// 3.创建 ROS 句柄
32ros::NodeHandle nh;
33// 4.创建订阅者对象
34ros::Subscriber sub = nh.subscribe("/turtle1/pose",1000,doPose);
35// 5.回调函数处理订阅的数据
36// 6.spin
37ros::spin();
38return 0;
39}

<左右滑动以查看完整代码>

配置文件此处略。


实现方案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实现流程:
10    1.导包
11    2.初始化 ROS 节点
12    3.创建订阅者对象
13    4.回调函数处理订阅的数据
14    5.spin
15
16"""

17
18import rospy
19from turtlesim.msg import Pose
20
21def doPose(data):
22rospy.loginfo("乌龟坐标:x=%.2f, y=%.2f,theta=%.2f",data.x,data.y,data.theta)
23
24if __name__ == "__main__":
25
26# 2.初始化 ROS 节点
27rospy.init_node("sub_pose_p")
28
29# 3.创建订阅者对象
30sub = rospy.Subscriber("/turtle1/pose",Pose,doPose,queue_size=1000)
31#     4.回调函数处理订阅的数据
32#     5.spin
33rospy.spin()

<左右滑动以查看完整代码>


权限设置以及配置文件此处略。


3.运行


首先,启动roscore;

然后启动乌龟显示节点,执行运动控制节点;

最后启动乌龟位姿订阅节点;

最终执行结果与演示结果类似。



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


结果演示:



实现分析:


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

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

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


实现流程:


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

2.编码实现服务请求节点。

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


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


获取话题

1/spawn
2rosservice list

<左右滑动以查看完整代码>





获取消息类型:


1turtlesim/Spawn
2rosservice type /spawn

<左右滑动以查看完整代码>


获取消息格式:


rossrv info turtlesim/Spawn

<左右滑动以查看完整代码>


响应结果:


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

<左右滑动以查看完整代码>


2.服务客户端实现


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


实现方案A:C++

1/*
2生成一只小乌龟
3准备工作:
4    1.服务话题 /spawn
5    2.服务消息类型 turtlesim/Spawn
6    3.运行前先启动 turtlesim_node 节点
7
8实现流程:
9    1.包含头文件
10      需要包含 turtlesim 包下资源,注意在 package.xml 配置
11    2.初始化 ros 节点
12    3.创建 ros 句柄
13    4.创建 service 客户端
14    5.等待服务启动
15    6.发送请求
16    7.处理响应
17
18*/

19
20#include "ros/ros.h"
21#include "turtlesim/Spawn.h"
22
23int main(int argc, char *argv[])
24
{
25setlocale(LC_ALL,"");
26// 2.初始化 ros 节点
27ros::init(argc,argv,"set_turtle");
28// 3.创建 ros 句柄
29ros::NodeHandle nh;
30// 4.创建 service 客户端
31ros::ServiceClient client = nh.serviceClient("/spawn");
32// 5.等待服务启动
33// client.waitForExistence();
34ros::service::waitForService("/spawn");
35// 6.发送请求
36turtlesim::Spawn spawn;
37spawn.request.x = 1.0;
38spawn.request.y = 1.0;
39spawn.request.theta = 1.57;
40spawn.request.name = "my_turtle";
41bool flag = client.call(spawn);
42// 7.处理响应结果
43if (flag)
44{
45    ROS_INFO("新的乌龟生成,名字:%s",spawn.response.name.c_str());
46else {
47    ROS_INFO("乌龟生成失败!!!");
48}
49
50
51return 0;
52}

<左右滑动以查看完整代码>


配置文件此处略。


实现方案B:Python


1#! /usr/bin/env python
2"""
3生成一只小乌龟
4准备工作:
5    1.服务话题 /spawn
6    2.服务消息类型 turtlesim/Spawn
7    3.运行前先启动 turtlesim_node 节点
8
9实现流程:
10    1.导包
11      需要包含 turtlesim 包下资源,注意在 package.xml 配置
12    2.初始化 ros 节点
13    3.创建 service 客户端
14    4.等待服务启动
15    5.发送请求
16    6.处理响应
17
18"""

19
20import rospy
21from turtlesim.srv import Spawn,SpawnRequest,SpawnResponse
22
23if __name__ == "__main__":
24# 2.初始化 ros 节点
25rospy.init_node("set_turtle_p")
26# 3.创建 service 客户端
27client = rospy.ServiceProxy("/spawn",Spawn)
28# 4.等待服务启动
29client.wait_for_service()
30# 5.发送请求
31req = SpawnRequest()
32req.x = 2.0
33req.y = 2.0
34req.theta = -1.57
35req.name = "my_turtle_p"
36try:
37    response = client.call(req)
38    # 6.处理响应
39    rospy.loginfo("乌龟创建成功!,叫:%s",response.name)
40except expression as identifier:
41    rospy.loginfo("服务调用失败")

<左右滑动以查看完整代码>


权限设置以及配置文件此处略。


3.运行


首先,启动roscore;

然后启动乌龟显示节点;

最后启动乌龟生成请求节点;

最终执行结果与演示结果类似。



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


结果演示:



实现分析:


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

  • 要通过ROS命令,来获取参数服务器中设置背景色的参数。

  • 编写参数设置节点,修改参数服务器中的参数值。


实现流程:


1.通过ros命令获取参数。

2.编码实现服参数设置节点。

3.启动roscore、turtlesim_node 与参数设置节点,查看运行结果。


1.参数名获取


获取参数列表:rosparam list


响应结果:

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

<左右滑动以查看完整代码>


2.参数修改


实现方案A:C++


1/*
2注意命名空间的使用。
3
4*/

5#include "ros/ros.h"
6
7
8int main(int argc, char *argv[])
9
{
10ros::init(argc,argv,"haha");
11
12ros::NodeHandle nh("turtlesim");
13//ros::NodeHandle nh;
14
15// ros::param::set("/turtlesim/background_r",0);
16// ros::param::set("/turtlesim/background_g",0);
17// ros::param::set("/turtlesim/background_b",0);
18
19nh.setParam("background_r",0);
20nh.setParam("background_g",0);
21nh.setParam("background_b",0);
22
23
24return 0;
25}

<左右滑动以查看完整代码>


配置文件此处略。


实现方案B:Python


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

<左右滑动以查看完整代码>


权限设置以及配置文件此处略。


3.运行


首先,启动roscore;

然后启动背景色设置节点;

最后启动乌龟显示节点;

最终执行结果与演示结果类似。


PS:注意节点启动顺序,如果先启动乌龟显示节点,后启动背景色设置节点,那么颜色设置不会生效。


4.其他设置方式


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

1rosparam set /turtlesim/background_b 自定义数值
2rosparam set /turtlesim/background_g 自定义数值
3rosparam set /turtlesim/background_r 自定义数值

<左右滑动以查看完整代码>


修改相关参数后,重启turtlesim_node节点,背景色就会发生改变了。


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


rosrun turtlesim turtlesim_node _background_b:=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
8    <!-- launch 传参策略2 -->
9    <rosparam command="load" file="$(find demo03_test_parameter)/cfg/color.yaml" />
10</node>
11
12</
13

<左右滑动以查看完整代码>


三种通信机制中,参数服务器是一种数据共享机制,可以在不同的节点之间共享数据,Topic与Service是在不同的节点之间传递数据,二者是ROS中最基础也是应用最为广泛的通信机制,有一定的相似性也有本质上的差异,在此做一下简单比较:


二者的实现流程也是比较相似的,都是涉及到四个要素:


  • 要素1: 消息的发布方/客户端(Publisher/Client)。

  • 要素2:消息的订阅方/服务端(Subscriber/Server)。

  • 要素3: 话题名称(Topic/Service)。

  • 要素4: 数据载体(msg/srv)。


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


二者的实现也是有本质差异的,具体比较如下:



Topic(话题)

Service(服务)

通信模式

发布/订阅

请求/响应

同步性

异步

同步

底层协议

ROSTCP/ROSUDP

ROSTCP/ROSUDP

缓冲区

时时性

节点关系

多对多

一对多(一个 Server)

通信数据

msg

srv

使用场景

连续高频的数据发布与接收:雷达、里程计

偶尔调用或执行某一项特定功能:拍照、语音识别


不同通信机制有一定的互补性,都有各自适应的应用场景。尤其是话题与服务通信,需要结合具体的应用场景与二者的差异,选择合适的通信机制。




本章主要介绍了ROS中最基本的也是最核心的通信机制实现: 话题通信、服务通信、参数服务器。每种通信机制,都介绍了如下内容:


  • 伊始介绍了当前通信机制的应用场景

  • 介绍了当前通信机制的理论模型

  • 分别介绍了当前通信机制的C++与Python实现


最后,还介绍了ROS中的常用命令方便操作、调试节点以及通信信息,并着重比较了话题通信与服务通信的相同点以及差异。当然,在ROS中上述三种通信协议并不能完全满足ROS中多样的应用场景,下一章ROS通信机制进阶篇,将继续介绍上述通信机制存在的缺陷,以及优化策略。


以上是"ROS通信机制"的全部内容,更多话题讨论、技术交流可以扫描下方二维码添加『Apollo小哥哥』为好友,进开发者交流群。






* 以上内容为开发者原创,不代表百度官方言论,已获开发者授权。




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存