1、rclcpp::spin(node)背后可以怎么理解?
简单理解:
spin(node) ↓ 创建/使用 Executor ↓ 把 Node 加进去 ↓ 持续等待 ROS2 事件 ↓ 执行 callbackSingleThreadedExecutor
用一个线程依次执行 callback。
比如 Node 有:
Callback A Callback B Callback C可能执行:
A ↓ B ↓ C同一时间只有一个 callback 在跑。
MultiThreadedExecutor
一句话:
允许多个 worker thread 并发执行 callback。
例如:
Callback A Callback B Callback C可以:
Thread1 → A Thread2 → B Thread3 → C同时执行。
于是慢 callback 不一定堵死所有 callback。
MutuallyExclusive
同一个 group 里的 callback:
不要同时执行例如:
Callback A Callback B都修改:
robot_state_放进:
MutuallyExclusive可以避免它们同时跑。
Reentrant
表示:
允许并行执行甚至某些情况下同一个 callback 的多个实例也可能并发。
所以使用它之前:
代码必须真的线程安全不能因为名字听起来高级就全用 Reentrant。
消息到DDS ↓ 变成可处理事件 ↓ Executor等待/发现 ↓ 调度callback ↓ callback开始执行所以:
消息到达时间和:
callback真正执行时间之间可能有延迟。
这个延迟和:
Executor 线程数 其他callback CPU负载 Callback Group都有关系。
Linux Process │ ├── Executor │ ├── Worker Thread 1 │ └── Worker Thread 2 │ ├── Node A │ ├── Subscription callback │ └── Timer callback │ ├── Node B │ ├── Service callback │ └── Subscription callback │ └── 自定义Worker ThreadsExecutor:
管理Node里的callback执行而 Linux:
真正调度这些线程所以:
ROS2 Executor ↓ 线程 ↓ Linux scheduler ↓ CPU这条链一定要记住。
Node = ROS2功能实体 Process = Linux运行实体 Executor = 调度callback Callback = 真正执行的业务函数2、Topic
Topic = ROS2 里用于“持续数据流”的发布/订阅通信机制。
它们的特点是:
持续产生 ↓ 持续消费例如:
动捕 50 Hz ↓ 不断发布人体姿态这就是 Topic。
Publisher 和 Subscriber 不是直接函数调用
这是非常重要的一点。
很多初学者会脑补成:
Publisher ↓ 直接调用 Subscriber callback实际上不是。
更接近:
Publisher ↓ rclcpp ↓ RMW ↓ DDS ↓ 网络 / 共享内存 / 本机传输 ↓ DDS ↓ RMW ↓ Subscriber ↓ Executor ↓ Callback所以中间有很多层。
这也是为什么:
Publisher 存在 Subscriber 也存在不代表:
消息一定成功进入 callbackRMW
ROS Middleware可以简单理解:
ROS2 对底层 DDS 的统一接口层。
ROS2 上层:
publisher->publish(msg);你不用直接操作 DDS。
因为:
rclcpp ↓ RMW ↓ 具体 DDS 实现帮你做了。
所以 ROS2 可以支持不同中间件实现。
例如你以后可能见到:
Fast DDS Cyclone DDS但上层代码基本不用完全重写。
DDS
DDS 负责发现、匹配和传输发布/订阅数据。
包括:
谁在发布什么 Topic 谁在订阅什么 Topic 消息类型是什么 QoS 是否兼容 数据怎么送过去所以:
ROS2 Topic并不是单纯:
一个 socket它背后是 DDS 通信系统。
MocapDriverNode ↓ publisher.publish(msg) ↓ rclcpp ↓ RMW ↓ DDS Writer ↓ 传输 ↓ DDS Reader ↓ RMW ↓ rclcpp Subscription ↓ Executor发现callback ready ↓ 执行 gmr callback所以延迟可能来自:
发布端 DDS 网络 订阅端 Executor callback不是只来自网络。
QoS 不兼容
这和你之前实际排查过的问题高度相关。
例如:
Publisher reliability = best_effortSubscriber:
reliability = reliable如果 QoS 请求/提供关系不兼容:
双方能在 graph 中出现但:
消息可能匹配失败于是:
Publisher count=1 Subscriber count=1仍然可能:
收不到数据所以:
Graph 连接存在,不代表 QoS 数据通路一定成立。
Topic 不适合所有通信
Topic 适合:
连续数据流但如果是:
“帮我执行一次 reset”Topic 也能勉强做。
但更合理可能是:
Service如果是:
“执行一个需要几秒的轨迹,并持续反馈进度”更适合:
Action所以你要慢慢形成:
Topic = streaming Service = request / response Action = long-running task后面逐个讲。
多机 ROS2 网络还可能受什么影响?
例如:
DDS discovery multicast 防火墙 网卡选择 路由 Docker VPN都可能影响。
所以多机通信排查要分层:
Linux网络 ↓ DDS发现 ↓ ROS2 graph ↓ QoS ↓ Topic实际数据 ↓ callback你之前学 Linux 网络,就是为了这一层。
一个完整 Topic 排查流程
假设:
/mocap/human_pose没数据。
第一步:
ros2 node list确认:
Publisher node 在不在 Subscriber node 在不在第二步:
ros2 topic list确认 Topic 在不在。
第三步:
ros2 topic info /mocap/human_pose看:
type publisher count subscriber count QoS第四步:
ros2 topic hz /mocap/human_pose确认:
实际有没有数据 频率多少第五步:
ros2 topic echo /mocap/human_pose看内容是否正常。如果 ROS2 层确认没发布数据呢?继续往 Publisher 内部查:
上游设备有数据吗? ↓ socket recv 有数据吗? ↓ parser 成功吗? ↓ queue 有数据吗? ↓ publish()真的执行了吗?可以用:
ss strace 日志 gdb所以排查路径:
ROS2 ↓ C++ ↓ Linux全部打通。
Topic 数据路径里哪些地方可能 copy?
概念上可能涉及:
用户消息对象 ↓ ROS2序列化 ↓ DDS buffer ↓ 网络/共享内存 ↓ 反序列化 ↓ Subscriber消息所以大型消息:
图像 点云复制成本可能很高。
这也是为什么 ROS2 有:
intra-process communication loaned messages shared memory transport等优化方向
所以“Topic没数据”只是表象。
你应该拆成:
有没有生产? ↓ 有没有传输? ↓ 有没有接收? ↓ 有没有执行?3、Service 是什么?
Service = Client 发一次请求,Server 返回一次响应。
例如:
reset_statistics set_mode enable_motor get_status都很适合 Service。
基本关系:
Client ↓ request Server ↓ response ClientTopic 和 Service 最大区别
Topic:
Publisher ↓ 持续发送数据 ↓ Subscriber更像:
广播数据流Service:
Client ↓ “帮我做一件事 / 告诉我一个结果” ↓ Server ↓ 返回结果所以:
Topic = streaming Service = request / response适合:
快速请求 ↓ 快速返回例如:
reset() get_status() set_mode() reload_config() clear_fault()这些操作通常:
毫秒级或者很快完成。Service 不适合:
需要几十秒执行 需要持续进度 需要取消 需要反馈例如:
机械臂走到目标点 执行一条轨迹 导航到某处 标定过程 长时间动作这些应该优先考虑:
ActionService 的 request / response 是自定义接口
例如:
ResetStatistics.srv可能:
Request --- bool success string message或者:
SetMode.srv int32 mode --- bool success string message这就是自定义:
.srv文件。
中间:
---上面:
Request下面:
Response例如:
int32 mode --- bool success string message意思:
Client发 mode Server返回 success + messageCLI 怎么调用 Service?
例如:
ros2 service list看服务。
看类型:
ros2 service type /set_mode查看接口定义:
ros2 interface show my_interfaces/srv/SetMode调用:
ros2 service call /set_mode my_interfaces/srv/SetMode "{mode: 1}"这些是很实用的调试工具。
什么时候用 Service?
你以后拿到一个功能需求,可以问:
这是持续数据吗?是:
Topic不是,再问:
这个请求能很快完成吗?是:
Service如果:
执行时间长 需要进度 需要取消:
Action这个判断非常实用。
Action 和 Service 最大区别
先直接对比:
Service = 请求一次 → 等结果 → 很快结束Action = 发一个目标 Goal → 任务持续执行 → 中途不断 Feedback → 最后 Result → 过程中还能 Cancel所以:
短任务 → Service 长任务 → Action4、Action
例如:
执行轨迹 导航到目标点 机械臂移动到某个姿态 自动标定 执行抓取 回零流程这些任务有共同特点:
不是瞬间完成而且你往往希望知道:
执行到多少了? 还剩多久? 成功了吗? 失败了吗? 能不能中途取消?这就是 Action。Action 有四个核心概念
Goal Feedback Result Cancel分别是:
Goal = 我要你做什么Feedback = 现在做到哪了Result = 最终结果是什么Cancel = 停止这个任务这四个是 Action 的核心。
Action Server 也依赖 Executor
Action Server 内部也有:
Goal callback Cancel callback Feedback Result handling这些都和:
Executor Callback Group有关系。
所以如果执行逻辑写得很差:
Action也一样会卡常见思路:
Goal callback ↓ 快速接收并验证 ↓ 交给 worker thread / state machine ↓ 后台执行 ↓ 周期发送 feedback ↓ 完成后 result这样 Executor 可以继续处理:
Cancel 其他Topic ServiceAction Server 本质上像状态机
你可以把一个 Goal 想成:
Received ↓ Accepted ↓ Executing ↓ Succeeded或者:
Executing ↓ Canceled或者:
Executing ↓ Aborted所以 Action 天然带有:
任务生命周期的概念。
Succeeded / Aborted / Canceled
Succeeded = 正常完成Canceled = 收到取消请求并停止Aborted = 任务因为错误无法继续例如:
执行轨迹中 ↓ 电机fault不是:
Canceled更像:
Aborted这种状态区分很有工程价值。
Action 和 Safety Layer 怎么连接?
例如执行轨迹:
Action Goal ↓ trajectory planner/executor ↓ 实时 command ↓ Safety Layer ↓ EtherCAT如果执行过程中:
joint limit触发 communication timeout driver fault那么:
Action Server应该收到下层状态,最终:
Abort Goal而不是继续显示:
Executing所以 Action 是上层任务接口,Safety 是底层保护。
Action 和 Topic 可以一起工作
例如:
Action → 管理整条轨迹任务同时:
Topic → 持续发布机器人状态这两个不是互斥的。
比如:
/execute_trajectory Action /joint_states Topic /diagnostics Topic职责各自清楚。
CLI 怎么看 Action?
可以:
ros2 action list看所有 Action。
看类型:
ros2 action type /execute_trajectory查看接口:
ros2 interface show my_interfaces/action/ExecuteTrajectory发送 Goal:
ros2 action send_goal ...还可以要求显示 Feedback。
Topic = 数据流例:
/joint_states /mocap/human_poseService = 快速问答例:
/reset_statistics /clear_faultAction = 长任务 + Feedback + Result + Cancel例:
/execute_trajectory /calibrate这三个是 ROS2 接口设计最基础的判断框架。
5、Parameter
Parameter 和 Launch / YAML
Node 里:
声明参数Launch / YAML:
传具体值可以理解:
Node = 定义有哪些配置项 YAML = 保存一组配置 Launch = 启动时把配置灌进去这是非常重要的工程分层。
Parameter 最适合放什么?
很适合:
端口 IP 频率 timeout queue_capacity source_type 文件路径 开关 阈值 QoS配置 日志等级这些都属于:
部署配置。
参数回调是什么?
ROS2 可以注册参数修改 callback。
例如:
有人 ros2 param set ↓ Node收到参数变更请求 ↓ callback检查是否合法 ↓ Accept / Reject例如:
queue_capacity = -10明显不合法。
应该:
Reject而不是接受后程序崩掉。
Parameter 和环境变量区别
环境变量:
export ROBOT_IP=...也是配置方式。
但 ROS2 Parameter 有:
Node级管理 CLI查看 Launch集成 YAML集成 运行时修改所以 ROS2 节点自身配置通常优先 Parameter。
而环境变量更适合:
系统级环境 路径 中间件选择 ROS_DOMAIN_ID等。
Parameter 和 config 文件区别
其实 YAML Parameter 本身就是一种配置文件方式。
但第三方 SDK 可能还有:
厂商自己的json/yaml/xml此时可以:
ROS2 Parameter ↓ 告诉程序 config_file 路径 ↓ 程序读取厂商配置不一定要把所有第三方配置都强行拆成 ROS 参数。
Parameter 也是 API
这是很重要的系统工程思维。
一个 Node 对外不仅有:
Topic Service Action还有:
Parameter所以 Node 的公共接口可以画成:
Node API ├── Topics ├── Services ├── Actions └── Parameters这就是为什么参数命名、默认值、约束都要认真设计。
6、Launch
Launch 管什么?最常见就是:
启动哪些 Node 给每个 Node 什么参数 放在哪个 namespace Topic 怎么 remap 用什么配置文件 什么时候启动 是否条件启动所以它不是业务逻辑。
它是:
部署逻辑。
概念上:
Node( package='my_pkg', executable='mocap_driver', name='mocap_driver', parameters=[...], )你先不用背完整语法。
只要知道:
package = 哪个包 executable = 哪个程序 name = 运行时Node名字 parameters = 参数代码 ↓ 定义能力 YAML ↓ 保存配置 Launch ↓ 组织系统这个三层结构非常重要。
Namespace
假设你有两台机器人:
robot1 robot2同一个 Node:
joint_state_publisher如果都叫:
/joint_states就容易混。
可以通过 namespace:
/robot1/joint_states /robot2/joint_states于是同一套软件就能复用。
namespace 就是给名字加一层“前缀”,用来避免重名冲突,或者把相关的东西组织在一起。
Remap
把 Node 原本使用的 ROS 名字,在运行时改成另一个名字。
比如代码里订阅:
cmd_vel但系统实际控制器需要:
/diff_drive_controller/cmd_velLaunch 可以做:
cmd_vel → /diff_drive_controller/cmd_vel而无需改 C++。
Name 和 Namespace 不一样
例如:
namespace = /robot1 name = mocap_driver最终 Node 可能是:
/robot1/mocap_driver而 Topic:
human_pose可能最终变成:
/robot1/human_pose所以:
Node name和:
Namespace要分开理解。
Launch Arguments
你可以理解成:
启动整套系统时的“外部参数”。
比如:
use_sim robot_name config_file source_type启动时:
ros2 launch my_pkg bringup.launch.py use_sim:=true于是同一个 Launch:
真机 仿真 不同机器人都能复用。
robot_bringup ├── launch │ ├── bringup.launch.py │ ├── mocap.launch.py │ └── control.launch.py └── config ├── mocap.yaml ├── control.yaml └── robot.yaml这种结构很常见。机器人里bringup通常表示:
把整套系统拉起来包括:
驱动 状态发布 控制器 传感器 诊断所以你以后看到:
xxx_bringup一般就知道是部署/启动包。
Launch 和 systemd
这个很重要。
Launch:
管理 ROS2 系统内部节点组合systemd:
管理 Linux 服务生命周期可以形成:
systemd ↓ ros2 launch robot_bringup bringup.launch.py ↓ 启动多个 ROS2 Node也就是:
Linux服务层 → systemd ROS2系统编排层 → Launch两者不是替代关系。
7、QoS
Quality of Service可以理解成:
这条 ROS2 通信链路的“传输规则”。
例如同样是 Topic:
/joint_states你可能希望:
最新数据最重要 偶尔丢一帧可以而:
/critical_command你可能希望:
尽量不要丢这两个 Topic:
数据类型可能一样但通信要求完全不同。
这就是 QoS。
Volatile
意思:
Subscriber 加入以后,只接收之后的新消息。
例如 Publisher:
10:00 发布 message ASubscriber:
10:01 才启动那么:
message A通常不会自动补给它。
它只能等:
之后的新消息这就是:
VolatileTransient Local
可以粗略理解:
Publisher 会保留一定历史数据,让后加入的 Subscriber 也有机会拿到。
例如:
10:00 发布 robot_description/config/state 10:01 Subscriber才启动如果配置成合适的:
Transient LocalSubscriber可能还能得到之前保存的数据。
有些信息:
不是高频一直发但后启动节点也必须知道。
例如某些:
静态配置 一次性状态 静态变换这种情况下:
Transient Local就有意义。
Deadline
Deadline 可以理解:
我期望数据至少多久来一次。
例如:
期望 10 ms 一条但:
超过10 ms还没来就说明:
deadline missed这非常适合表达:
通信周期要求Deadline 和 command timeout
非常像,但层级不同。
QoS Deadline:
DDS / ROS2通信层告诉你:
预期周期没有满足Command Timeout:
应用Safety层决定:
超过100ms没新command ↓ safe stop所以推荐:
Deadline → 通信监控 Command timeout → 安全动作不要认为有 Deadline 就不用 Safety timeout。
8、TF
header.frame_id
比如:
sensor_msgs/LaserScan sensor_msgs/PointCloud2 geometry_msgs/PoseStamped里面经常有:
header.frame_id例如:
frame_id = "camera_link"意思:
这份位置/数据是在 camera_link 坐标系下表达的。
如果frame_id错了:
数值可能完全没错但整个系统解释就会错。
TF 树
机器人里通常可以想象成:
map └── odom └── base_link ├── imu_link ├── camera_link ├── lidar_link ├── left_leg_link └── right_leg_link这就是:
TF tree每个 frame 都和另一个 frame 建立父子关系。
最重要的三个 frame:map / odom / base_link
这是移动机器人里最经典的一组:
map ↓ odom ↓ base_linkbase_link
机器人身体本体坐标系。
比如移动机器人:
机器人中心人形机器人可能定义在:
pelvis 躯干 base具体取决于模型。
但核心:
base_link = 机器人本体参考坐标系odom
odom 是一个局部连续坐标系。
特点:
短时间连续 不会突然跳但会:
长期漂移因为它往往来自:
轮式里程计 IMU积分 视觉里程计这些都会积累误差。
所以:
odom → base_link告诉你:
根据局部运动估计,机器人现在相对起点在哪里。
map
map可以理解成:
全局参考坐标系。
例如 SLAM / Localization 告诉你:
机器人在整张地图里的真实位置它长期更准确。
但有一个问题:
定位算法重新校正时 可能产生跳变例如原来估计:
x = 5.2定位算法突然发现:
其实应该是 4.8那:
map → base_link直接变化会“跳”。
常见结构:
map ↓ odom ↓ base_link其中:
odom → base_link保持:
局部连续而:
map → odom负责:
修正长期漂移这样:
控制可以继续使用连续的 odom。
而:
导航仍然知道全局 map 位置。
Static TF 和 Dynamic TF
Static TF
固定不变。
比如:
base_link → camera_link base_link → imu_link base_link → lidar_link只要硬件没有重新安装:
位置关系不变这种就是:
static transform通常发布到:
/tf_staticDynamic TF
会随时间变化。
比如:
odom → base_link机器人在运动:
位置一直变还有机械臂:
base_link → shoulder_link → elbow_link → wrist_link关节转动后:
link之间关系不断变化这些是:
dynamic transform通常走:
/tf/tf 和 /tf_static 区别
简单记:
/tf = 动态坐标变换/tf_static = 静态坐标变换比如:
odom → base_link发/tf
而:
base_link → camera_link可以发/tf_static
robot_state_publisher
它根据:
URDF + joint_states计算:
机器人各个 link 之间的 TF例如:
base_link ↓ left_hip_link ↓ left_knee_link ↓ left_ankle_link关节角变化时:
TF跟着变化Listener = 收TF Buffer = 存TF lookupTransform = 查TFtf2_echo
例如:
ros2 run tf2_ros tf2_echo base_link imu_link如果正常:
translation rotation会不断显示。
如果失败:
Could not transform...你就知道:
TF链路有问题四十五、Fixed Frame 是什么?
RViz 里经常有:
Fixed Frame例如:
map odom base_link它表示:
RViz最终把所有数据转换到哪个统一坐标系显示。
如果 Fixed Frame 选:
map那所有数据最终都需要能够:
transform → maporigin
子关节/子 link 的安装位置相对 parent 在哪里。
例如:
<origin xyz="0 0 0.3" rpy="0 0 0"/>意思:
相对 parent 向 z 方向 0.3 m并且初始没有旋转。
你可以理解成:
机械安装关系xyz 和 rpy
xyz:
x y z表示平移。
rpy:
roll pitch yaw表示旋转。
所以:
<origin xyz="0.2 0 0.5" rpy="0 0 1.57"/>就表示:
平移 + 旋转这和上一课 TF2 是完全对应的。
axis
比如:
<axis xyz="0 0 1"/>表示这个关节围绕:
Z轴转。
如果:
<axis xyz="0 1 0"/>就是绕 Y 轴。
所以如果机器人关节方向反了:
axis就是必须检查的地方之一。
prismatic joint 直线运动关节
例如:
滑轨 升降机构 伸缩机构不是角度变化,而是:
线位移URDF + joint_states 怎么变成 TF?
关键角色:
robot_state_publisher它做:
读取URDF ↓ 知道机械结构 ↓ 订阅joint_states ↓ 知道当前关节角 ↓ 计算各link当前变换 ↓ 发布TF这就是完整逻辑。
robot_description
可以理解成:
URDF 内容作为一个 ROS 参数提供给系统。
通常:
robot_state_publisher会拿到:
robot_description然后解析整个机器人模型。
Xacro
Xacro 可以简单理解:
带变量、宏、条件逻辑的 URDF 模板。
因为纯 URDF XML 很容易:
重复 很长 难维护所以用 Xacro 简化。
Xacro 里的 macro
你可以把 macro 理解成:
URDF 的函数模板。
比如:
make_leg(name, side)生成:
hip knee ankle一整套 link/joint。
Xacro ↓ 解析 ↓ URDF ↓ robot_state_publisher所以:
Xacro = 生成URDF的工具collision
描述:
这个 link 用什么几何体参与碰撞检测。
它不一定和 visual 完全一样。
例如视觉 mesh 很复杂:
100万三角形碰撞可能只用:
简单box/cylinder提高效率。
inertial
<inertial> 告诉 ROS 2 这个连杆的质量、质心位置、转动惯量。没有它,机器人就无法正确仿真和动力学计算。
mass center of mass inertia动力学仿真会用到。
比如:
MuJoCo Gazebo 动力学计算如果惯性参数乱写:
仿真可能非常奇怪但单纯 RViz 显示可能暂时看不出来。
JointStateBroadcaster 和 robot_state_publisher 不一样
这个以后 ros2_control 会反复遇到。
joint_state_broadcaster主要负责:
从硬件接口读取关节状态 ↓ 发布 /joint_states然后:
robot_state_publisher拿/joint_states:
计算 TF所以:
Hardware ↓ joint_state_broadcaster ↓ /joint_states ↓ robot_state_publisher ↓ /tf这条链以后一定要熟。
/controller_manager /joint_state_broadcaster /robot_state_publisher现在你就应该知道:
controller_manager = 管控制器和硬件接口joint_state_broadcaster = 把关节状态发出来robot_state_publisher = 根据URDF+joint_states生成TF这三个不是重复功能。
URDF 和 TF2 怎么对应?
URDF 中:
parent child origin最终就是在描述:
parent frame ↓ child frame的几何关系。
robot_state_publisher:
把URDF结构 转换成动态TF所以上一课和这一课其实是一套东西。
真实硬件 ↓ Hardware Interface ↓ joint_state_broadcaster ↓ /joint_states ↓ robot_state_publisher ↑ URDF ↓ /tf ↓ RViz控制另一边:
JointTrajectory / Controller ↓ Hardware Interface ↓ EtherCAT ↓ 电机这两条链:
状态链 + 控制链最终在 ros2_control 汇合。