为了解耦合,ROS中每一个功能点都是一个单独的进程,每个进程独立运行,ROS是进程的分布式框架
- 话题通信(发布订阅模式) 比如抖音上关注一个话题,会给话题关注者推送
- 服务通信(请求响应模式) 客户端先请求服务器,服务器再响应客户端,如访问网址
- 参数服务器(参数共享模式) 一个公共容器DATA,所有节点都可以在里面自由拿或放数据
话题通信:
引入:比如机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成控制信息驱动底盘
导航模块中有节点订阅并解析雷达数据;底盘中也有一个信号订阅运动信息并转为脉冲
导航相对于雷达是订阅方,相对于底盘是发布方
用于不断更新,少逻辑处理的应用场景
角色
ROS Master:管理者 Talker:发布者 Listener:订阅者
流程图上比较清楚,用自己的理解说下,Talker首先把话题和RPC远程地址给Master,Listener把自己关注的话题给Master,比对如果两者一样,Master把Talker的远程访问地址给Listener,之后Listener通过这个远程地址要到发布者的TCP地址,建立数据连接
注意:
使用的协议有RPC与TCP
步骤0与步骤1没有顺序关系
建立TCP连接后,就没Master什么事了,可以关闭
实现的话调用就行,已经被封装
首先还是新建ws,之后改下CMake编译文件的内容,创建新的功能包,在包下面新建scripts文件夹,新建.py文件
看发布的内容用 rostopic echo che
之后学习用固定的HZ数发送:现在初始化部分初始化一个频率rate=rospy.Rate(1) 1就是1HZ 之后在循环里调用rate.sleep( )实现定时发送
设置计数器,计数器初始化部分count=0 循环里面count+=1 ;msg.data = “hello”+str(count)
再来rospy.loginfo(“发布的数据是:%s”,msg.data)
#! usr/bin/envs python
import rospy
from std_msgs.msg import String
if __name__ == "__main__":
rospy.init_node("ergou")
pub = rospy.Publisher("che",String,queue_size=10)
msg=String()
rata=rospy.Rate(1)
count=0
while not rospy.is_shutdown():
count+=1
msg.data="caole"+str(count)
pub.publish(msg)
rospy.loginfo("输出为%s",msg.data)
rata.sleep()
这个是发送节点
定义接收节点,要在定义sub的时候就定义好回调函数
#! /usr/bin/env python
import rospy
from std_msgs.msg import String
def doMsg(msg):
rospy.loginfo("我订阅的数据是:%s",msg.data)
if __name__ =="__main__":
rospy.init_node("huahua")
sub=rospy.Subscriber("che",String,doMsg,queue_size=10)
rospy.spin()
关键是不要忘记.spin( )没有这个回调函数就没法工作
细节上的处理,有一种情况,当订阅方早早启动,发布方后启动,此时依然会漏掉一些信息,这是因为发布方要先在Master那里注册,解决办法是在发之前加一个小休眠,rospy.sleep(3)
rqt_gragh输出计算图
二狗节点和花花节点,车是两人的话题
解耦合特点:C++发送发与python接受方可以实现数据交换
下面的例子试一下复杂信息,比如姓名;年龄;身高复合信息,每种信息的格式也是不一样
一般我们要自定义msg,类似C语言中的结构体,可以使用的数据类型有整型,浮点型,字符串,时间相关的,其他的消息文件,数组,header标头等
编辑自定义的msg
- 创建msg目录与文件
- 编辑配置文件
package.xml中
60多行的位置,message_generation与message_runtime
Cmakelist.txt里要配置的比较多
- 编译生成可被python和C++调用的中间文件
编译完之后在devel文件夹下include生成头文件person.h,C++调用
_person.py是python用的
首先要配置vscode,将前面生成的python文件路径配置进setting.json
pwd查看路径,添加到setting里面
repub.py
#! /usr/bin/env python
import rospy
from pub_sub.msg import person
if __name__ == "__main__":
rospy.init_node("ergou")
pub=rospy.Publisher("che",person,queue_size=10)
rata=rospy.Rate(1)
count=0
p=person()
p.name="奥特曼"
p.height=173
while not rospy.is_shutdown():
count+=1
p.age=count
pub.publish(p)
rata.sleep()
rospy.loginfo("%s",p.name)
resub.py
#! /usr/bin/env python
import rospy
from pub_sub.msg import person
def doperson(da):
rospy.loginfo("收到的是:%s,%d,%.2f",da.nmae,da.age,da.height)
if __name__ =="__main__":
rospy.init_node("huahua")
sub=rospy.Subscriber("che",person,doperson)
rospy.spin()
‘
服务通信:基于请求响应模式,是一种应答机制,比如识别到疑似指针表,需要拍照,一个节点需要向相机节点发送拍照请求,相机节点处理请求并返回结果
用于偶然的,对实时性有要求,有一定逻辑处理需求的场景
Mast er还是会根据话题建立服务端与客户端的连接,之后就可以TCP通信了
Master这里类比114,匹配需求与服务,左边是公司注册,要提供通信地址,右边是用户,只给出需求的话题
注意:
- 顺序需要注意,客户端发起请求时,服务端需要已经启动
- 客户端与服务端都可以存在多个
接下来开始实操,首先是创建srv文件,在功能包下新建srv文件夹,Addint.srv文件,与之前不同,他要包含两个数据,发送部分和响应部分之间用三个杠分割---
package.xml
之后Cmakelist.txt
生成了对应头文件
开发时还是要先配置下setting.json文件,将生成的中间文件添加进来
首先编写服务端,解析客户请求,产生响应
- 导包
- 初始化ROS节点
- 创建服务端对象
- 处理逻辑(回调函数)
- Spin( )
Service.[y
#! /usr/bin/env python
import rospy
from server_client.srv import*
def donum(re):
num1=re.num1
num2=re.num2
sum=num1+num2
respons=AddintsResponse()
respons.sum=sum
rospy.loginfo("结果是%d",sum)
return respons
if __name__ == "__main__":
rospy.init_node("heishui")
server=rospy.Service("add",Addints,donum)
rospy.loginfo("开机")
rospy.spin()
注意这些新的srv,msg都要在setting.json中配置新路径
"/home/rosenoetic/demo03_ws/devel/lib/python3/dist-packages"
Client.py
#! /usr/bin/env python
import rospy
from server_client.srv import *
if __name__ == "__main__":
rospy.init_node("ergo")
client=rospy.ServiceProxy("add",Addints)
respon=client.call(12,32)
rospy.loginfo("%d",respon.sum)
改进,把原来写死的数字改为命令行键入
使用sys.argv本例有三个参数 [0]是文件名字
#! /usr/bin/env python
import rospy
from server_client.srv import *
import sys
if __name__ == "__main__":
if len(sys.argv)!=3:
rospy.logerr("输入的个数不对")
sys.exit(1)
rospy.init_node("ergo")
client=rospy.ServiceProxy("add",Addints)
num1=int(sys.argv[1])
num2=int(sys.argv[2])
respon=client.call(num1,num2)
rospy.loginfo("%d",respon.sum)
注意:这种实现下要求Service先启动,Client再启动才可以使用,但ROS节点启动是不保证顺序的,我们要改进下,如果Service没启动Client先挂起,实现就是在发送前加入
client.wait_for_service()
或者用rospy的一个函数,等待指定话题的服务
rospy.wait_for_service("add")
参数服务器用于实现不同节点之间的参数共享,相当于独立于所有节点的公共容器,可以将数据存储在该容器,被不同节点调用,当然不同节点也可以往里存放数据
全局路径规划:设计一个从出发点到目标点的大概路径,比如回龙观到故宫
本地路径规划:根据当前路况生成的实时行进路径
这两种服务都会使用到参数服务器,比如我们把小车的尺寸信息存储到参数服务器,规划时调用出来
类似于全局变量吧
Talker设置方 Listener调用方 整体通信效率一般,建议存储简单静态数据(如unit32,时间iso数据,布尔数据等)
首先是第一类操作:参数的增与改
调用rospy.set_param( ) 并在命令行调用rosparam list查看有哪些项目 调用rosparam get r查看r的值
#! usr/bin/env python
import rospy
if __name__ == "__main__":
rospy.init_node("seet")
rospy.set_param("type","AGV_CAR")
rospy.set_param("r",0.15)
修改值操作跟设置一样的,就是重新set刷新下值
之后编写查询代码
import rospy
if __name__ == "__main__":
rospy.init_node("serachh")
rr=rospy.get_param("r",5.0)
rr1=rospy.get_param("r1",5.0)
rospy.loginfo("%.2f",rr)
rospy.loginfo("%.2f",rr1)
对于可以查到的返回正常值,查不到的返回一个默认值
r=rospy.get_param_cached("r",5.0)
这个是更高效的一种查询方式,如果在缓存直接有的数据会更快查到
for name in names:
rospy.loginfo("name=%s",name)
这个是用来查询所有名字
flag=rospy.has_param("r")
这个是用来判断是否有这个键
最后看删除,只有一个函数
#! /usr/bin/env python
import rospy
if __name__ == "__main__":
rospy.init_node("delate")
rospy.delete_param("r")
就不会报错了
常用命令行,当自定义节点与已有的节点通信时,需要获取话题以及消息载体的格式,可以用命令行的指令完成
Rosnode -h,什么-h都是查看帮助
rosnode info打印的信息比较全面,后期常用
rostopic echo如果消息载体是自定义的,则需要切换到目标目录,刷新下环境变量再运行,扮演接收方的角色
rostopic pub 话题之后两次自动补齐就可以充当发布方
rostopic pub -r 10就是以一秒10次的频率发送
rostopic info可以查看比较详细的信息
rostopic hz可以显示发布频率
对于客户服务通信,rosservice info充当客户,要注意还是得配置使用的环境变量才可以TABLE补齐
发布订阅模型msg查询,比较鸡肋,也得配置环境变量
客户服务模型rossrv查询,与msg语法高度神似
rosparam可以实现YAML文件与参数服务器交互,以及对参数服务器的更删改查操作
还可以加载回来
之后开始乌龟项目来加深对通信的理解
案例一,替换键盘控制(通过命令行和计算图获取话题和消息),写一个小程序控制乌龟运动
先获取格式,再看格式具体
线速度和角速度,本案例线速度只设置X,角速度只设置Z即可
X:前进或后退 Y:水平方向 Z:垂直方向 角速度单位弧度每秒
偏航:围绕Z轴转 翻滚:围绕X轴转 俯仰:围绕Y轴转
import rospy
from geometry_msgs.msg import Twist
if __name__ =="__main__":
rospy.init_node("control1")
pub=rospy.Publisher("/turtle1/cmd_vel",Twist,queue_size=10)
rate=rospy.Rate(10)
twist=Twist()
twist.linear.x=1.0
twist.linear.y=0.0
twist.linear.y=0.0
twist.angular.x=0.0
twist.angular.y=0.0
twist.angular.z=1.0
while not rospy.is_shutdown():
pub.publish(twist)
rate.sleep()
第二个例子是订阅,为了方便打开乌龟GUI和键盘,创建一个launch文件夹以及launch文件,用来启动这两个节点
<launch>
<node pkg="turtlesim" name="turtle1" type="turtlesim_node" output="screen"/>
<node pkg="turtlesim" name="key" type="turtle_teleop_key" output="screen"/>
</launch>
用roslaunch直接启动
可以看到包含坐标,朝向,线速度和角速度
#! /usr/bin/env python
import rospy
from turtlesim.msg import Pose
def chuli(p):
rospy.loginfo("x=%.2f,y=%.2f",p.x,p.y)
if __name__ == "__main__":
rospy.init_node("retrunn")
sub=rospy.Subscriber("/turtle1/pose",Pose,chuli,queue_size=100)
rospy.spin()
可以接受到了,原点在GUI左下角
需要的参数有四个 坐标,朝向以及名字
#! /usr/bin/env python
import rospy
from turtlesim.srv import *
if __name__ =="__main__":
rospy.init_node("sheng")
cliant=rospy.ServiceProxy("/spawn",Spawn)
sp=SpawnRequest()
sp.x=5.0
sp.y=5.0
sp.theta=0.0
sp.name="turtle4"
cliant.wait_for_service()
try:
cliant.call(sp)
except Exception as e:
rospy.loginfo("error")
注意操作有.srv import *还有try: except Exception as e:
参数服务器案例:修改乌龟背景色
rospy.setparam