2. ROS通信机制详解:话题通信与服务通信
好,咱们今天来聊聊ROS里最核心的东西——通信机制。说实话,我刚开始接触ROS的时候,最头疼的就是搞明白话题和服务到底有什么区别。你想想看,一个机器人系统里,传感器要发数据,电机要收指令,这些节点之间怎么说话?嗯,这就是我们今天要解决的问题。
2.1 话题通信:Publisher与Subscriber
话题通信,说白了就是“发布-订阅”模式。一个节点发布消息,其他节点订阅这个消息。就像电台广播一样,主播只管说,听众只管听,双方不需要知道对方是谁。
核心特点:异步通信,一对多,实时性强。
2.1.1 发布者(Publisher)的实现
我个人习惯先写发布者,因为它是数据的源头。来看一个简单的例子,发布一个“速度指令”的话题:
# publisher.py
import rospy
from geometry_msgs.msg import Twist
def talker():
rospy.init_node('velocity_publisher', anonymous=True)
pub = rospy.Publisher('/cmd_vel', Twist, queue_size=10)
rate = rospy.Rate(10) # 10Hz
while not rospy.is_shutdown():
msg = Twist()
msg.linear.x = 0.5
msg.angular.z = 0.2
pub.publish(msg)
rate.sleep()
if __name__ == '__main__':
try:
talker()
except rospy.ROSInterruptException:
pass
这里要注意几个点:queue_size参数很关键。我在项目中遇到过,如果订阅者处理速度跟不上发布者,消息就会堆积。queue_size设得太小会丢消息,设得太大又占内存。一般10-100之间比较合适。
2.1.2 订阅者(Subscriber)的实现
订阅者就简单多了,等着收消息就行:
# subscriber.py
import rospy
from geometry_msgs.msg import Twist
def callback(msg):
rospy.loginfo("收到速度指令: 线速度=%.2f, 角速度=%.2f",
msg.linear.x, msg.angular.z)
def listener():
rospy.init_node('velocity_listener', anonymous=True)
rospy.Subscriber('/cmd_vel', Twist, callback)
rospy.spin()
if __name__ == '__main__':
listener()
嗯,这里有个坑。我曾经在调试底盘时,发现订阅者收不到消息。查了半天,原来是话题名字拼错了。ROS不会报错,只是默默等待。所以,话题名字一定要一致,这是新手最容易犯的错误。
避坑指南:我曾经因为话题名多了一个斜杠,排查了整整两个小时。建议用rostopic list命令确认话题是否存在。
2.2 服务通信:Server与Client
话题通信是“只管发不管收”,但有些场景需要一问一答。比如,你让机器人去抓一个物体,它抓到了没有?这时候就需要服务通信了。
服务通信是同步的。客户端发请求,服务端处理完返回响应。说白了,就是“你问我答,答完结束”。
2.2.1 服务端(Server)的实现
先定义一个服务。ROS里服务用.srv文件定义,比如:
# AddTwoInts.srv
int64 a
int64 b
---
int64 sum
然后实现服务端:
# server.py
import rospy
from beginner_tutorials.srv import AddTwoInts, AddTwoIntsResponse
def handle_add(req):
result = req.a + req.b
rospy.loginfo("收到请求: %d + %d = %d", req.a, req.b, result)
return AddTwoIntsResponse(result)
def server():
rospy.init_node('add_two_ints_server')
s = rospy.Service('add_two_ints', AddTwoInts, handle_add)
rospy.spin()
if __name__ == '__main__':
server()
2.2.2 客户端(Client)的实现
客户端就负责发请求:
# client.py
import rospy
from beginner_tutorials.srv import AddTwoInts, AddTwoIntsRequest
def client():
rospy.wait_for_service('add_two_ints')
try:
add_two_ints = rospy.ServiceProxy('add_two_ints', AddTwoInts)
resp = add_two_ints(3, 5)
rospy.loginfo("结果: %d", resp.sum)
except rospy.ServiceException as e:
rospy.logerr("服务调用失败: %s", e)
if __name__ == '__main__':
client()
个人经验:我建议在客户端调用前,先用rospy.wait_for_service()等待服务就绪。否则服务还没启动,客户端就发请求,会直接报错。
2.3 自定义消息与服务
ROS自带的消息类型够用吗?说实话,大部分场景够用。但有时候你需要传递一些特殊数据,比如“机器人姿态+电池电量+错误码”。这时候就得自定义消息了。
2.3.1 自定义消息
在msg文件夹下创建RobotStatus.msg:
# RobotStatus.msg
string robot_name
float32 battery_level
int32 error_code
geometry_msgs/Pose pose
然后在CMakeLists.txt里添加:
add_message_files(FILES RobotStatus.msg)
generate_messages(DEPENDENCIES geometry_msgs)
编译后就能用了。我在做巡检机器人时,就自定义了一个PatrolStatus.msg,包含了位置、电量、温度等信息,调试起来特别方便。
2.3.2 自定义服务
自定义服务和消息类似,在srv文件夹下创建SetSpeed.srv:
# SetSpeed.srv
float64 linear_x
float64 angular_z
---
bool success
string message
然后同样在CMakeLists.txt里添加:
add_service_files(FILES SetSpeed.srv)
2.4 话题与服务的选择
很多新手会纠结:什么时候用话题,什么时候用服务?我给大家一个简单的判断标准:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 传感器数据持续发布 | 话题 | 实时性高,一对多 |
| 控制指令下发 | 话题 | 需要持续更新 |
| 查询状态或执行一次性任务 | 服务 | 需要返回结果 |
| 配置参数 | 服务 | 一问一答,确认成功 |
核心总结:话题是“广播”,服务是“电话”。广播只管说,电话要等对方回应。
2.5 实战中的注意事项
最后,分享几个我在实际项目中踩过的坑:
- 消息类型匹配:发布者和订阅者的消息类型必须完全一致,包括包名。我曾经把
geometry_msgs/Twist写成了std_msgs/Twist,编译不报错,运行没反应。 - 频率控制:发布频率不是越高越好。我见过有人把激光雷达数据以100Hz发布,结果CPU直接跑满。一般传感器数据10-50Hz就够了。
- 服务超时:服务调用默认会一直等待。如果服务端卡住了,客户端也会卡住。建议设置超时时间:
rospy.ServiceProxy('service_name', ServiceType, timeout=5)。
好了,这一章的内容就到这里。通信机制是ROS的基石,搞懂了它,后面的内容就顺了。下一章我们聊聊如何用launch文件管理节点,让启动变得优雅起来。