第1章:ROS基础概念——节点、话题、服务、参数、消息

各位同学,欢迎来到ROS的世界。我是你们的讲师,一个在机器人堆里摸爬滚打了十来年的老工程师。

今天咱们要聊的,是ROS最核心的五个概念:节点、话题、服务、参数、消息。说白了,这五个东西就是ROS的“五脏六腑”。你搞懂了它们,后面搭建仿真环境、写控制程序,就会顺风顺水。要是没搞懂……嗯,后面你会回来重新看这一章的。

1.1 节点(Node)——机器人的“小工人”

节点是什么?我习惯把它想象成一个“小工人”。每个节点只干一件事,比如控制左轮、处理激光雷达数据、发布里程计信息。

在ROS里,一个机器人系统通常由几十个甚至上百个节点组成。每个节点都是独立的可执行程序。它们之间互不干扰,各司其职。

为什么要把程序拆成这么多节点?

你想想看,如果所有功能都写在一个程序里,一旦某个模块崩溃,整个机器人就挂了。但用节点的方式,某个节点挂了,其他节点还能继续跑。我在项目里遇到过激光雷达节点偶尔闪退的情况,但底盘控制节点还在正常工作,机器人至少能安全停下来。这就是节点化的好处。

创建一个节点很简单,用Python的话就几行代码:

#!/usr/bin/env python3
import rospy

rospy.init_node('my_first_node')  # 初始化节点,名字叫my_first_node
rospy.loginfo("节点已启动!")
rospy.spin()  # 让节点保持运行

注意节点名字不能重复。同一个ROS系统里,节点名是唯一的标识。

我的小技巧:给节点起名字时,我习惯用“功能_模块”的格式,比如lidar_drivermotor_controller。这样一看名字就知道它是干什么的。

1.2 话题(Topic)——节点间的“广播站”

节点之间怎么通信?最常用的方式就是话题。

话题就像广播站。一个节点往话题里发消息(发布者),其他节点从话题里收消息(订阅者)。发布者和订阅者互相不知道对方是谁,它们只关心话题本身。

举个例子:

激光雷达节点不断发布/scan话题,里面装着激光扫描数据。导航节点订阅/scan,拿到数据后做路径规划。这两个节点完全解耦,互不依赖。

我曾经调试过一个项目,发现激光雷达数据时有时无。排查了半天,原来是发布者的频率设置得太低,订阅者等不及就超时了。所以,发布频率和订阅频率要匹配好。

发布话题的代码长这样:

pub = rospy.Publisher('chatter', String, queue_size=10)
rate = rospy.Rate(10)  # 10Hz
while not rospy.is_shutdown():
    pub.publish("hello world")
    rate.sleep()
重点记住:话题通信是异步的、一对多的。一个发布者,多个订阅者,完全没问题。

1.3 服务(Service)——节点间的“电话通话”

话题是单向广播,服务则是双向的请求-响应模式。就像你打电话:你问一句,对方答一句,有来有回。

什么时候用服务?当你需要某个节点执行一个操作,并且等待结果时。比如:

  • 请求机器人拍照,返回图片数据
  • 请求机械臂移动到某个位置,返回是否成功
  • 请求获取当前电池电量,返回百分比

服务的定义包含两部分:请求(Request)和响应(Response)。

写一个服务端:

def handle_add_two_ints(req):
    return AddTwoIntsResponse(req.a + req.b)

rospy.Service('add_two_ints', AddTwoInts, handle_add_two_ints)

写一个客户端:

rospy.wait_for_service('add_two_ints')
try:
    add_two_ints = rospy.ServiceProxy('add_two_ints', AddTwoInts)
    resp = add_two_ints(3, 5)
    print(resp.sum)
except rospy.ServiceException as e:
    print("服务调用失败: %s" % e)
避坑指南:我曾经在服务端忘记处理异常,结果客户端一直卡在等待状态。记住,服务调用一定要加超时或异常处理,否则一个节点挂了,整个系统都可能卡死。

1.4 参数(Parameter)——机器人的“小本本”

参数服务器就像一个全局的小本本。节点可以把配置信息写进去,也可以从里面读出来。

比如:机器人的轮子直径、最大速度、PID参数、摄像头分辨率……这些都可以放在参数里。这样你改参数时不用重新编译代码,改一下配置文件就行。

我习惯把参数分成两类:

  • 静态参数:基本不变,比如机器人型号、传感器类型
  • 动态参数:调试时经常改,比如速度上限、控制增益

操作参数的常用命令:

# 设置参数
rospy.set_param('max_speed', 1.5)

# 读取参数
speed = rospy.get_param('max_speed')

# 检查参数是否存在
if rospy.has_param('max_speed'):
    print("参数存在")

# 删除参数
rospy.delete_param('max_speed')
我的习惯:启动文件(launch file)里统一设置所有参数,这样一目了然。别在代码里硬编码参数值,否则换台机器人就得改代码。

1.5 消息(Message)——节点间的“通用语言”

节点之间传数据,总得有个统一的格式吧?消息就是干这个的。

ROS定义了很多标准消息类型,比如:

消息类型 说明 常用场景
std_msgs/String 字符串 日志、简单指令
geometry_msgs/Twist 速度指令(线速度+角速度) 控制机器人移动
sensor_msgs/LaserScan 激光雷达扫描数据 避障、建图
nav_msgs/Odometry 里程计数据(位置+速度) 定位、导航

你也可以自定义消息。比如你做了一个特殊的传感器,需要传一些自定义数据,那就自己定义一个.msg文件。

自定义消息的格式:

# 文件: MySensorData.msg
float32 temperature
float32 humidity
uint16 light_intensity
string sensor_id

然后在代码里就能像使用标准消息一样使用它。

记住:消息是ROS通信的“血液”。所有节点之间的数据交换,最终都归结为消息的发布、订阅、请求、响应。

1.6 五个概念的关系——一张图说清楚

这五个概念不是孤立的,它们协同工作:

  • 节点是执行者,负责干活
  • 话题是广播通道,节点通过它发布和订阅消息
  • 服务是双向通道,节点通过它发送请求和接收响应消息
  • 参数是全局配置,所有节点都能读写

说白了,整个ROS系统就是一堆节点,通过话题和服务交换消息,同时从参数服务器读取配置。就这么简单。

我记得刚学ROS那会儿,总觉得这些概念很抽象。后来动手写了一个小机器人控制程序,把激光雷达数据通过话题发给导航节点,导航节点再通过话题发布速度指令给底盘节点——那一刻,我才真正理解了它们是怎么配合的。

所以,别光看,动手试试。下一章咱们就开始搭建Gazebo仿真环境,到时候这些概念都会用上。

课后练习:打开终端,运行roscore,然后新开一个终端运行rosrun turtlesim turtlesim_node。再用rostopic list看看有哪些话题在跑。这就是一个活生生的ROS系统。