2、ROS通信机制:话题通信(Publisher与Subscriber)、服务通信(Server与Client)、自定义消息与服务
好,咱们直接进入正题。ROS的通信机制,说白了就是机器人各个零件之间怎么“说话”。你想想看,一个机器人有激光雷达、有摄像头、有电机驱动,它们得互相配合才行。那它们怎么交换数据?靠的就是今天要讲的这套东西。
我个人习惯把ROS通信分成两大类:话题通信和服务通信。前者是“一直说”,后者是“问一句答一句”。嗯,咱们一个一个来。
2.1 话题通信:Publisher与Subscriber
话题通信,是ROS里最常用的模式。一个节点发布消息,其他节点订阅这个消息。就像电台广播一样,主播一直在说话,听众想听就打开收音机。
Publisher(发布者):负责发送数据。比如激光雷达节点,它每秒发几十次扫描数据。
Subscriber(订阅者):负责接收数据。比如导航节点,它订阅激光数据来做避障。
我在项目中遇到过一个问题:两个节点明明在同一个网络里,但就是收不到对方的消息。后来发现是话题名字拼写错了。嗯,这种低级错误我犯过一次就再也没忘过。
核心要点:话题通信是异步的、一对多的。发布者只管发,不管有没有人收。订阅者只管收,不管谁发的。
来看一个最简单的发布者代码示例:
# publisher.py
import rospy
from std_msgs.msg import String
rospy.init_node('talker')
pub = rospy.Publisher('chatter', String, queue_size=10)
rate = rospy.Rate(10) # 10Hz
while not rospy.is_shutdown():
msg = String()
msg.data = "hello world %s" % rospy.get_time()
pub.publish(msg)
rate.sleep()
对应的订阅者:
# subscriber.py
import rospy
from std_msgs.msg import String
def callback(msg):
rospy.loginfo("I heard: %s", msg.data)
rospy.init_node('listener')
sub = rospy.Subscriber('chatter', String, callback)
rospy.spin()
你看,发布者用 publish() 发,订阅者用回调函数收。就这么简单。
小技巧:queue_size参数很关键。如果发布速度比处理速度快,消息会积压在队列里。设得太小会丢消息,设得太大占内存。我一般设10-100之间,看具体场景。
2.2 服务通信:Server与Client
话题通信是“一直说”,服务通信就是“一问一答”。客户端发一个请求,服务端处理完返回一个响应。适合那些需要即时反馈的操作,比如“打开机械臂”、“查询当前位姿”。
Server(服务端):提供某种功能,等待别人来调用。
Client(客户端):发起请求,等待服务端回复。
我曾经在调试一个抓取任务时,客户端一直收不到响应。查了半天,发现是服务端在处理请求时死循环了。嗯,服务端代码一定要加超时处理,不然客户端会一直等下去。
服务通信的定义文件是 .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("Returning %s + %s = %s", req.a, req.b, result)
return AddTwoIntsResponse(result)
rospy.init_node('add_two_ints_server')
s = rospy.Service('add_two_ints', AddTwoInts, handle_add)
rospy.spin()
客户端代码:
# client.py
import rospy
from beginner_tutorials.srv import AddTwoInts, AddTwoIntsRequest
rospy.init_node('add_two_ints_client')
rospy.wait_for_service('add_two_ints')
add = rospy.ServiceProxy('add_two_ints', AddTwoInts)
resp = add(4, 5)
print("Sum = %s" % resp.sum)
注意:客户端调用服务前,最好用 rospy.wait_for_service() 确保服务已经就绪。不然会抛出异常。我刚开始写的时候经常忘记这步,结果程序一启动就崩了。
2.3 自定义消息与服务
ROS自带的 std_msgs 只有基本类型:整型、浮点、字符串等。但实际项目中,我们经常需要传递复杂结构。比如一个“目标点”消息,包含x、y坐标和朝向角度。这时候就需要自定义消息。
自定义消息放在 msg/ 目录下,文件后缀是 .msg。比如:
# msg/TargetPoint.msg
float64 x
float64 y
float64 theta
string frame_id
然后在 CMakeLists.txt 里加上:
add_message_files(FILES TargetPoint.msg)
generate_messages(DEPENDENCIES std_msgs)
自定义服务类似,放在 srv/ 目录下。我习惯把相关的消息和服务放在同一个包里,方便管理。
经验之谈:自定义消息的字段名要起得清晰。别用 a、b 这种,过两周你自己都看不懂。我一般用 pose_x、velocity_linear 这种带单位或含义的名字。
另外,自定义消息编译后会自动生成Python和C++的代码。你只需要 import 进来就能用。比如:
from my_package.msg import TargetPoint
msg = TargetPoint()
msg.x = 1.0
msg.y = 2.0
msg.theta = 0.5
2.4 话题 vs 服务:怎么选?
很多新手会纠结:什么时候用话题,什么时候用服务?我简单总结一下:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 传感器数据(激光、图像) | 话题 | 持续更新,多个节点需要 |
| 控制指令(速度、位置) | 话题 | 需要实时性,持续发送 |
| 查询状态(电池电量、位姿) | 服务 | 按需获取,不需要持续监听 |
| 执行动作(打开夹爪、开始导航) | 服务 | 需要反馈结果,一次性的 |
说白了,如果你需要“一直听”,用话题。如果你需要“问一下然后等答案”,用服务。就这么简单。
避坑指南:我曾经在一个项目里用话题来传递“打开阀门”的指令。结果因为话题是异步的,指令发了但没人处理,阀门没打开。后来改成服务通信,客户端等到了确认才继续执行。嗯,从此我对“同步 vs 异步”有了深刻理解。
2.5 总结
今天咱们聊了ROS通信的两种基本方式:话题和服务。话题是“广播”,服务是“问答”。自定义消息和服务让你能传递复杂数据。这些都是ROS机器人开发的基石。
下一章咱们会讲参数服务器和TF变换。到时候你会发现,通信机制和坐标变换结合起来,才能真正让机器人动起来。嗯,先消化今天的内容吧。