ROS 2 期末复习全解析(知识点 + 编程题)
我会按照你的要求,先详细讲解所有核心知识点(配生动解释),再给出编程题的完整代码 + 逐行解释,最后做扩展总结,确保你能彻底理解并应对考试。
一、ROS 2 QoS 核心参数详解 + 四类话题 QoS 取向
1. QoS 三大核心参数(生动解释)
QoS(Quality of Service)是 ROS 2 中话题通信的 "通信规则",就像你和朋友聊天:有的消息必须确保收到(如转账确认),有的丢了也没关系(如实时直播弹幕)。
| 参数 | 核心含义 | 生动类比 | 关键取值 |
|---|---|---|---|
| Reliability(可靠性) | 是否确保消息 100% 送达 | 寄快递:- Reliable = 顺丰保价(丢件必赔,确保送到)- Best Effort = 普通平邮(能送就送,丢了不补) | - RELIABLE:保证所有消息送达(重传机制)- BEST_EFFORT:不保证,丢了就丢了(低延迟) |
| Durability(耐久性) | 发布者是否缓存消息,给 "后启动的订阅者" | 发朋友圈:- Transient Local = 发朋友圈后保留(晚刷手机的朋友也能看到)- Volatile = 发完即删(只有实时刷的人能看到) | - TRANSIENT_LOCAL:发布者缓存最后 N 条消息,新订阅者启动后立刻收到- VOLATILE:不缓存,只给 "在线" 的订阅者发 |
| History/Depth(历史 / 深度) | 发布者缓存多少条历史消息 | 朋友圈保留多少条动态:Depth=5 就是只保留最近 5 条 | - KEEP_LAST:只保留最近 N 条(Depth=N,默认 10)- KEEP_ALL:保留所有(慎用,占内存) |
2. 四类话题的 QoS 取向(考试重点)
| 话题类型 | 业务特点 | Reliability | Durability | History/Depth | 选择理由 |
|---|---|---|---|---|---|
| 激光点云(/scan) | 高频(10-20Hz)、实时性要求高,丢 1-2 帧不影响 | BEST_EFFORT | VOLATILE | KEEP_LAST(1) | 点云数据量大,实时性优先,丢帧不影响避障 / 建图;后启动的订阅者不需要旧点云 |
| 图像流(/image) | 超大带宽、实时性极高,丢帧可接受 | BEST_EFFORT | VOLATILE | KEEP_LAST(1) | 图像数据量最大,RELIABLE 会导致重传卡顿;只需要最新帧,无需缓存 |
| 控制指令(/cmd_vel) | 低频(10-50Hz)、必须送达,丢指令会导致机器人失控 | RELIABLE | VOLATILE | KEEP_LAST(1) | 控制指令是核心,必须 100% 送达;后启动的控制器不需要旧指令(要最新的) |
| 静态外参 / 标定参数(/calib_params) | 低频(只发 1 次 / 启动时发)、所有节点必须拿到 | RELIABLE | TRANSIENT_LOCAL | KEEP_LAST(1) | 标定参数是静态的,必须确保所有节点(包括后启动的)都能拿到;缓存 1 条足够 |
扩展知识点
- QoS 不匹配问题:如果发布者设为 RELIABLE,订阅者设为 BEST_EFFORT,通信正常(向下兼容);反之则无法通信(ROS 2 会报错)。
- 实战建议:除了静态参数,大部分实时流(点云 / 图像)优先选 BEST_EFFORT+VOLATILE,控制类选 RELIABLE+VOLATILE。
二、差速两轮机器人运动学计算(核心公式 + 解释)
1. 核心参数定义(先明确变量)
- 左轮线位移增量:ΔsL(单位:m,向前为正,向后为负)
- 右轮线位移增量:ΔsR(单位:m)
- 轮距:B(单位:m,左右轮中心的距离)
2. 车体中心运动增量计算(生动解释:把机器人看成圆规)
差速机器人的运动可以拆解为 "平移"+"旋转",就像你用圆规画圆:两个轮的转速差决定旋转角度,平均转速决定平移距离。
(1)车体中心平移增量(Δs)
机器人中心的移动距离 = 左右轮移动距离的平均值:Δs=2ΔsL+ΔsR
(2)航向角增量(Δθ)
航向角(机器人朝向)的变化量 = 左右轮位移差 / 轮距(弧度):Δθ=BΔsR−ΔsL
- 若Δθ>0:机器人逆时针转(左转);
- 若Δθ<0:机器人顺时针转(右转);
- 若ΔsL=ΔsR:Δθ=0,机器人直线运动。
(3)平移增量的分解(可选,考试常考)
如果需要分解为 x/y 轴的平移(假设机器人初始朝向为 x 轴正方向):Δx=Δs⋅cos(θ+2Δθ)Δy=Δs⋅sin(θ+2Δθ)(θ为机器人初始航向角,2Δθ是因为旋转过程中中心的平均朝向)
示例计算(帮你理解)
假设:左轮位移ΔsL=0.1m,右轮位移ΔsR=0.15m,轮距B=0.2m
- 平移增量:Δs=(0.1+0.15)/2=0.125m
- 航向角增量:Δθ=(0.15−0.1)/0.2=0.25rad≈14.3°(机器人左转 14.3 度)
扩展知识点
- 单位转换:弧度转角度公式:角度=弧度×(180/π)
- 误差注意:实际机器人有打滑,计算值是 "理论值",需要结合里程计 / IMU 修正。
三、rosbag2 + use_sim_time //clock 机制
1. rosbag2 的核心作用(考试必背)
rosbag2 是 ROS 2 的 "数据记录仪",就像你开车时的行车记录仪:记录所有话题 / 服务 / 动作的消息,之后可以回放。核心用途:
- 回放调参:记录真实机器人的传感器数据(如 /scan),离线回放数据调避障算法参数(不用反复跑机器人);
- 复现实验:实验成功 / 失败后,回放数据复现整个过程,验证算法稳定性;
- 定位排错:机器人出问题时,回放 bag 文件,看某一时刻的传感器数据 / 控制指令,定位是算法 bug 还是硬件问题。
2. use_sim_time 与 /clock 的配合机制(生动解释)
(1)核心概念
use_sim_time:ROS 2 节点的一个参数(bool 型),默认false;开启后(true),节点不用 "系统本地时间",改用/clock话题的时间;/clock:一个话题(类型:rosgraph_msgs/msg/Clock),发布 "仿真时间"(由仿真器 /rosbag2 回放发布)。
(2)配合机制(一步一步讲)
-
什么时候开启 use_sim_time?
- 用仿真器(如 Gazebo、Webots)时:仿真世界有自己的时间(比如仿真 1 秒 = 现实 10 秒),所有节点必须用仿真时间,否则 TF、定时器会错乱;
- 用 rosbag2 回放数据时:bag 文件记录了消息的时间戳,开启后节点会按 bag 的时间线处理消息(比如回放 10 分钟的 bag,节点会 "以为" 时间过了 10 分钟);
- 总结:只要不是 "真实机器人实时运行",而是 "仿真 / 回放",就需要开启。
-
/clock 从哪里来?
- 仿真器(Gazebo):仿真启动后,Gazebo 节点会持续发布 /clock 话题,内容是当前仿真时间;
- rosbag2 回放:用
ros2 bag play --clock命令回放时,rosbag2 会从 bag 文件中读取时间戳,发布到 /clock 话题; - 自定义节点:你也可以写节点发布 /clock(极少用)。
-
对节点时间戳与 TF 的影响
- 时间戳:节点调用
get_clock()->now()时,返回的是 /clock 的时间(而非系统时间);话题消息的时间戳也会用 /clock 时间; - TF:TF 变换依赖时间戳,开启 use_sim_time 后,TF 树的所有变换都会基于 /clock 时间,确保仿真 / 回放时 TF 同步(比如机器人在仿真中移动 1 秒,TF 也更新 1 秒);
- 异常情况:如果开启 use_sim_time,但没有节点发布 /clock,节点会 "卡时间"------
now()返回 0,定时器不触发,TF 无法更新,所有和时间相关的功能都会失效。
- 时间戳:节点调用
3. 判断题解析(考试必考)
(1)"当启用 use_sim_time 后,如果系统没有发布 /clock,与时间相关的功能(如 TF、定时器、时间戳计算等)可能出现异常表现。"
✅ 正确解释:开启 use_sim_time 后,节点依赖 /clock 获取时间;如果没有 /clock,节点的时钟会停在 0,定时器(如每 1 秒触发)永远不触发,TF 变换的时间戳为 0,导致坐标转换错误,时间戳计算(如计算两帧点云的时间差)也会出错。
(2)"QoS 的 depth 设置得越大,系统的实时性就一定越好。"
❌ 错误解释:Depth 是发布者缓存的消息数,设置越大,占用的内存越多,发布者需要花更多时间管理缓存,反而会增加延迟,降低实时性;只有在 "需要缓存少量历史消息" 时,设置合适的 Depth(如 1-10)即可,不是越大越好。
四、编程题 1:Action Client 按顺序巡航目标点
1. 需求分析(先明确要实现的功能)
- 按顺序访问多个目标点,每个目标点等待 Action 返回结果;
- 目标点到达失败时自动重试;
- 输出 Action 的 feedback(实时反馈,如当前距离目标点的距离);
- 实现取消机制(这里选 "按键触发",简单易实现)。
2. 前置条件
- 假设使用 ROS 2 内置的
nav2_msgs/action/NavigateToPose(导航到目标点的 Action),你也可以替换为自定义 Action; - 依赖:安装 nav2(
sudo apt install ros-humble-nav2-msgs); - 环境:Python(rclpy),因为更易理解,考试中更常用。
3. 完整代码
python
运行
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped
import sys
import select
import tty
import termios
# 按键检测工具类(用于取消机制)
class KeyListener:
def __init__(self):
self.settings = termios.tcgetattr(sys.stdin)
def get_key(self):
"""检测是否按下键盘(非阻塞)"""
tty.setraw(sys.stdin.fileno())
rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
if rlist:
key = sys.stdin.read(1)
else:
key = ''
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.settings)
return key
# 巡航Action Client节点
class CruiseActionClient(Node):
def __init__(self):
super().__init__('cruise_action_client')
# 1. 创建Action Client(绑定NavigateToPose Action)
self._action_client = ActionClient(
self,
NavigateToPose,
'navigate_to_pose' # Nav2的导航Action话题名
)
# 2. 初始化变量
self.target_poses = [] # 目标点列表
self.current_target_idx = 0 # 当前处理的目标点索引
self.retry_max = 3 # 最大重试次数
self.key_listener = KeyListener() # 按键监听器
self.cancel_flag = False # 取消标志
def add_target_pose(self, x, y, yaw=0.0, frame_id='map'):
"""添加目标点(x,y为位置,yaw为航向角,frame_id为坐标系)"""
pose_stamped = PoseStamped()
pose_stamped.header.frame_id = frame_id
pose_stamped.header.stamp = self.get_clock().now().to_msg()
# 设置位置
pose_stamped.pose.position.x = x
pose_stamped.pose.position.y = y
pose_stamped.pose.position.z = 0.0
# 设置朝向(四元数,yaw转四元数)
from tf_transformations import quaternion_from_euler
q = quaternion_from_euler(0.0, 0.0, yaw)
pose_stamped.pose.orientation.x = q[0]
pose_stamped.pose.orientation.y = q[1]
pose_stamped.pose.orientation.z = q[2]
pose_stamped.pose.orientation.w = q[3]
self.target_poses.append(pose_stamped)
def send_goal(self, pose_stamped, retry_count=0):
"""发送单个目标点的Action请求,失败重试"""
# 检查取消标志
if self.cancel_flag:
self.get_logger().info('收到取消指令,停止发送目标点')
return
# 1. 等待Action Server上线
self._action_client.wait_for_server()
# 2. 构造Goal
goal_msg = NavigateToPose.Goal()
goal_msg.pose = pose_stamped
# 3. 发送Goal,注册feedback和result回调
self._send_goal_future = self._action_client.send_goal_async(
goal_msg,
feedback_callback=self.feedback_callback # feedback回调
)
# 4. 注册Goal发送结果的回调
self._send_goal_future.add_done_callback(self.goal_response_callback)
# 5. 保存当前重试次数和目标点(供回调使用)
self.current_retry = retry_count
self.current_pose = pose_stamped
def feedback_callback(self, feedback_msg):
"""Feedback回调:实时输出导航反馈"""
feedback = feedback_msg.feedback
# 输出关键反馈信息(距离目标点的剩余距离)
self.get_logger().info(
f'当前目标点({self.current_pose.pose.position.x}, {self.current_pose.pose.position.y}) '
f'反馈:距离目标点{feedback.distance_remaining:.2f}m,已完成{feedback.navigation_time.sec}秒'
)
# 检测取消按键(按'q'取消)
key = self.key_listener.get_key()
if key == 'q':
self.cancel_flag = True
self.get_logger().info('按下q键,触发取消机制!')
# 取消当前Action请求
self._action_client.cancel_goal_async(self._send_goal_future.result())
def goal_response_callback(self, future):
"""Goal发送结果回调:检查是否被接受"""
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().warn(f'目标点被拒绝!重试次数{self.current_retry+1}/{self.retry_max}')
# 重试逻辑
if self.current_retry < self.retry_max:
self.get_logger().info(f'重新发送目标点...')
self.send_goal(self.current_pose, self.current_retry + 1)
else:
self.get_logger().error(f'目标点重试{self.retry_max}次失败,跳过该目标点')
self.next_target()
return
self.get_logger().info('目标点被接受,等待导航结果...')
# 等待Action结果
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
"""Action结果回调:处理成功/失败"""
result = future.result().result
status = future.result().status
# Nav2的Action状态码:SUCCEEDED=4,FAILED=5
if status == 4:
self.get_logger().info(f'目标点({self.current_pose.pose.position.x}, {self.current_pose.pose.position.y}) 到达成功!')
self.next_target() # 处理下一个目标点
else:
self.get_logger().warn(f'目标点到达失败!状态码:{status},重试次数{self.current_retry+1}/{self.retry_max}')
# 重试逻辑
if self.current_retry < self.retry_max:
self.get_logger().info(f'重新发送目标点...')
self.send_goal(self.current_pose, self.current_retry + 1)
else:
self.get_logger().error(f'目标点重试{self.retry_max}次失败,跳过该目标点')
self.next_target()
def next_target(self):
"""处理下一个目标点"""
self.current_target_idx += 1
if self.current_target_idx < len(self.target_poses):
self.get_logger().info(f'开始处理第{self.current_target_idx+1}个目标点')
self.send_goal(self.target_poses[self.current_target_idx])
else:
self.get_logger().info('所有目标点处理完成!')
rclpy.shutdown()
def start_cruise(self):
"""启动巡航"""
if not self.target_poses:
self.get_logger().error('没有添加任何目标点!')
return
self.get_logger().info(f'开始巡航,共{len(self.target_poses)}个目标点,按q键取消')
self.send_goal(self.target_poses[self.current_target_idx])
def main(args=None):
rclpy.init(args=args)
# 1. 创建巡航客户端节点
cruise_client = CruiseActionClient()
# 2. 添加目标点(示例:3个目标点)
cruise_client.add_target_pose(x=1.0, y=0.0, yaw=0.0) # 目标点1:(1,0)
cruise_client.add_target_pose(x=1.0, y=1.0, yaw=1.57) # 目标点2:(1,1),朝向90度
cruise_client.add_target_pose(x=0.0, y=1.0, yaw=3.14) # 目标点3:(0,1),朝向180度
# 3. 启动巡航
cruise_client.start_cruise()
# 4. 自旋节点(处理回调)
rclpy.spin(cruise_client)
if __name__ == '__main__':
main()
4. 代码详细解释
(1)核心模块拆分
| 模块 | 作用 |
|---|---|
KeyListener |
非阻塞检测键盘按键(按 q 取消),实现取消机制; |
CruiseActionClient |
核心节点,管理目标点、发送 Action 请求、处理反馈 / 结果; |
add_target_pose |
封装目标点(转换为 ROS 2 的 PoseStamped 格式,包含位置和朝向); |
send_goal |
发送单个目标点的 Action 请求,包含重试逻辑; |
feedback_callback |
实时输出 Action 反馈(如剩余距离),并检测取消按键; |
goal_response_callback |
检查 Goal 是否被 Action Server 接受,拒绝则重试; |
get_result_callback |
处理 Action 结果,成功则下一个目标点,失败则重试; |
(2)关键功能解释
- 失败重试 :通过
retry_max设置最大重试次数,current_retry记录当前重试次数,失败后调用send_goal重新发送; - Feedback 输出 :
feedback_callback会被 Action Server 实时调用,输出distance_remaining(剩余距离)、navigation_time(导航时间)等核心反馈;如果你的 Action 接口没有这些字段,可以替换为打印feedback对象的所有属性(print(feedback)); - 取消机制 :通过
KeyListener非阻塞检测 q 键,按下后设置cancel_flag,并调用cancel_goal_async取消当前 Action 请求;
(3)运行说明
- 安装依赖:
pip install transforms3d(用于欧拉角转四元数); - 启动 Nav2(或自定义的 NavigateToPose Action Server);
- 运行代码:
ros2 run <包名> <节点名>; - 按 q 键可随时取消巡航。
5. 扩展知识点
- Action 的三种状态:Goal Accepted(接受)、Goal Executing(执行中)、Goal Finished(完成);
- 取消机制的其他实现方式:
- 参数触发:添加一个参数
cancel_cruise,定时检查参数值,改为 true 则取消; - 服务触发:创建一个服务
/cancel_cruise,调用该服务则取消;
- 参数触发:添加一个参数
- 重试逻辑优化:可以添加 "重试间隔"(如失败后等待 1 秒再重试),避免频繁重试。
五、编程题 2:多回调并发处理节点(/scan + /reset_pose + Timer)
1. 需求分析(核心是并发 / 隔离)
- /scan 订阅:回调耗时 m1(如 50ms),需要独立线程,避免阻塞其他回调;
- /reset_pose 服务:必须在 m2(如 10ms)内返回,需要最高优先级,独立隔离;
- Timer 定时器:每 m3(如 100ms)触发,输出状态,独立线程;
- 核心:用 Callback Group + MultiThreadedExecutor 实现隔离,确保服务的实时性。
2. 核心概念(先理解)
- Callback Group(回调组) :把不同的回调函数分组,每组有独立的执行规则:
MutuallyExclusiveCallbackGroup:组内回调串行执行(同一时间只能执行一个);ReentrantCallbackGroup:组内回调并行执行(同一时间可执行多个);
- MultiThreadedExecutor:多线程执行器,为不同的回调组分配线程,实现并发。
3. 完整代码
python
运行
import rclpy
from rclpy.node import Node
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup
from sensor_msgs.msg import LaserScan
from std_srvs.srv import Empty
import time
import threading
class ConcurrentNode(Node):
def __init__(self):
super().__init__('concurrent_node')
# --------------------------
# 1. 创建3个独立的回调组(按需求隔离)
# --------------------------
# /scan回调组:Reentrant(支持并行,因为/scan高频,耗时)
self.scan_callback_group = ReentrantCallbackGroup()
# /reset_pose服务组:MutuallyExclusive(串行,确保高优先级,快速响应)
self.service_callback_group = MutuallyExclusiveCallbackGroup()
# Timer回调组:MutuallyExclusive(串行,定时输出,无需并行)
self.timer_callback_group = MutuallyExclusiveCallbackGroup()
# --------------------------
# 2. 创建/scan订阅(绑定到scan回调组)
# --------------------------
self.scan_sub = self.create_subscription(
LaserScan,
'/scan',
self.scan_callback,
10, # QoS深度
callback_group=self.scan_callback_group # 绑定回调组
)
# --------------------------
# 3. 创建/reset_pose服务(绑定到service回调组)
# --------------------------
self.reset_pose_srv = self.create_service(
Empty,
'/reset_pose',
self.reset_pose_callback,
callback_group=self.service_callback_group # 绑定回调组
)
# --------------------------
# 4. 创建Timer(绑定到timer回调组)
# --------------------------
self.m3 = 1.0 # 定时器周期(秒),可修改为任意值
self.timer = self.create_timer(
self.m3,
self.timer_callback,
callback_group=self.timer_callback_group # 绑定回调组
)
# --------------------------
# 5. 状态变量
# --------------------------
self.scan_count = 0 # /scan回调次数
self.reset_count = 0 # /reset_pose调用次数
self.lock = threading.Lock() # 线程锁(保护共享变量)
def scan_callback(self, msg: LaserScan):
"""/scan回调:模拟耗时m1处理"""
self.get_logger().info(f'收到/scan消息,开始耗时处理...')
# 模拟耗时m1(如50ms)
m1 = 0.05 # 50毫秒
time.sleep(m1)
# 线程安全更新共享变量
with self.lock:
self.scan_count += 1
self.get_logger().info(f'/scan处理完成,累计收到{self.scan_count}次')
def reset_pose_callback(self, request: Empty.Request, response: Empty.Response):
"""/reset_pose服务回调:确保m2时间内返回"""
start_time = time.time()
self.get_logger().info('收到/reset_pose请求,快速处理...')
# 核心:服务处理必须在m2内完成(这里m2=0.01秒=10ms)
m2 = 0.01
# 模拟必要的处理(必须快速,耗时<m2)
with self.lock:
self.reset_count += 1
# 重置pose(示例:这里只是计数,实际可重置里程计等)
self.get_logger().info(f'重置pose完成,累计重置{self.reset_count}次')
# 检查处理耗时,确保在m2内
elapsed = time.time() - start_time
if elapsed > m2:
self.get_logger().warn(f'/reset_pose处理耗时{elapsed:.4f}秒,超过m2={m2}秒!')
else:
self.get_logger().info(f'/reset_pose处理耗时{elapsed:.4f}秒,符合m2要求')
return response
def timer_callback(self):
"""Timer回调:每m3秒输出状态"""
with self.lock:
self.get_logger().info(
f'=== 状态输出 ===\n'
f'/scan累计接收:{self.scan_count}次\n'
f'/reset_pose累计调用:{self.reset_count}次\n'
f'================'
)
def main(args=None):
rclpy.init(args=args)
# 1. 创建节点
node = ConcurrentNode()
# 2. 创建多线程执行器(关键:设置线程数,建议>=回调组数量)
executor = MultiThreadedExecutor(num_threads=4) # 4个线程足够处理3个回调组
executor.add_node(node)
try:
# 3. 自旋执行器(多线程处理回调)
executor.spin()
except KeyboardInterrupt:
pass
finally:
# 4. 清理资源
executor.shutdown()
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4. 代码详细解释
(1)核心设计思路
| 回调类型 | 回调组类型 | 绑定线程 | 设计理由 |
|---|---|---|---|
| /scan 订阅 | ReentrantCallbackGroup | 独立线程 | /scan 高频且耗时,Reentrant 支持并行处理(如果同时收到多个 /scan 消息),避免阻塞; |
| /reset_pose 服务 | MutuallyExclusiveCallbackGroup | 高优先级线程 | 服务需要快速响应,MutuallyExclusive 确保组内无并发,避免资源竞争,保证 m2 内返回; |
| Timer 定时器 | MutuallyExclusiveCallbackGroup | 独立线程 | 定时器输出状态,串行执行即可,避免重复输出; |
(2)关键代码解释
-
回调组创建:
python
运行
self.scan_callback_group = ReentrantCallbackGroup() # /scan并行 self.service_callback_group = MutuallyExclusiveCallbackGroup() # 服务串行 self.timer_callback_group = MutuallyExclusiveCallbackGroup() # 定时器串行每个回调组对应一个 "执行单元",MultiThreadedExecutor 会为每个组分配线程。
-
绑定回调组 :创建订阅 / 服务 / Timer 时,通过
callback_group参数绑定到对应的组,确保隔离:python
运行
self.scan_sub = self.create_subscription(..., callback_group=self.scan_callback_group) -
多线程执行器 :
MultiThreadedExecutor(num_threads=4)创建 4 个线程,足够处理 3 个回调组的并发,避免单线程阻塞。 -
线程安全 :使用
threading.Lock()保护共享变量(scan_count/reset_count),避免多线程同时修改导致数据错乱。 -
服务实时性保证:服务回调中记录开始时间,处理完成后计算耗时,确保在 m2 内;处理逻辑必须极简(如仅重置变量),避免耗时操作。
(3)运行说明
- 运行节点:
ros2 run <包名> <节点名>; - 测试 /reset_pose 服务:
ros2 service call /reset_pose std_srvs/srv/Empty; - 发布 /scan 消息:
ros2 topic pub /scan sensor_msgs/msg/LaserScan --once ...; - 观察日志:Timer 每 1 秒输出状态,/scan 处理耗时 50ms,服务处理耗时 < 10ms。
5. 扩展知识点
- 回调组选择原则:
- 高频 / 耗时回调:用 ReentrantCallbackGroup;
- 实时性要求高的回调(如服务):用 MutuallyExclusiveCallbackGroup;
- MultiThreadedExecutor 线程数:建议设置为回调组数量 + 1,避免线程过多导致调度开销;
- 耗时回调优化:如果 /scan 处理耗时极长(如 1 秒),可以把处理逻辑放到独立线程中,回调仅接收消息并放入队列,避免阻塞回调队列。
六、总结(考试重点回顾)
1. QoS 核心
- Reliability:控制类选 RELIABLE,传感器流选 BEST_EFFORT;
- Durability:静态参数选 TRANSIENT_LOCAL,实时流选 VOLATILE;
- Depth 不是越大越好,实时流选 1 即可。
2. 差速机器人运动学
- 平移增量:Δs=(ΔsL+ΔsR)/2;
- 航向角增量:Δθ=(ΔsR−ΔsL)/B;
- 核心:左右轮差速决定旋转,平均速度决定平移。
3. use_sim_time / /clock
- 开启场景:仿真 /rosbag2 回放;
- /clock 来源:仿真器 /rosbag2;
- 异常:开启后无 /clock 会导致时间相关功能失效。
4. 编程题核心
- Action Client:重试逻辑(计数 + 重新发送)、Feedback 回调、取消机制(按键 / 服务 / 参数);
- 并发节点:Callback Group 隔离、MultiThreadedExecutor 多线程、线程安全(锁)、服务实时性保证。