尧图精选

ROS2系列教程:认识节点与ROS图

🕒 发布时间:2026/9/4 6:48:32 📁 来源:尧图网络
本文是 ROS2 系列教程的第 2 篇本文是 ROS2 系列教程的第 2 篇认识节点与 ROS 图。上一篇文章我们搭建好了 Humble 环境并运行了第一个 Hello World 节点本篇文章将深入理解 ROS2 的最小单元——节点Node以及由节点连接而成的ROS 图ROS Graph。我们依然采用 Crclcpp与 Pythonrclpy双语言并讲每一节都配有可运行的完整示例。一、节点与 ROS 图核心概念1.1 什么是节点在 ROS2 中节点Node是一个负责执行特定任务的独立进程或进程内模块。一个机器人系统通常由几十个节点组成有的节点负责读取激光雷达数据有的负责做定位有的负责规划路径有的负责驱动电机。每个节点只干一件事把复杂系统拆解成大量小而专的单元这是 ROS 设计哲学的核心——单一职责 松耦合。一个节点至少包含以下基本要素节点名称在整个 ROS 图中全局唯一配合命名空间使用例如/sensor/lidar、/navigation/nav2_controller。通信端点节点通过话题Topic、服务Service、动作Action、参数Parameter与其他节点交互这些统称为通信端点。执行逻辑节点内部的回调函数、定时器、主循环决定了节点做什么。类比理解把 ROS2 比作一家公司节点就是公司里的员工。每个员工有明确的岗位节点名员工之间通过内部通讯系统话题/服务协作没有人是总管一切的中心ROS2 去中心化。1.2 什么是 ROS 图ROS 图ROS Graph是运行中的节点及其通信关系的抽象描述。它由三部分组成要素说明例子节点Node图的顶点/talker、/listener通信关系图的边talker 发布话题/chatter给 listener通信中间件DDS 层负责实际传输、发现、QoS 协商当你运行rqt_graph或在 RViz 中查看就能看到一张可视化的 ROS 图每个椭圆是一个节点每条箭头是一条话题连接箭头方向从发布者指向订阅者。这张图就是整个机器人系统的组织架构图。ROS2 与 ROS1 最大的区别之一ROS1 需要roscoreROS Master作为中央注册表所有节点必须向它注册ROS2没有中心节点节点之间通过 DDS 的 P2P 发现协议Discovery自动互相发现、直接通信。因此 ROS2 天然支持多机分布式部署——不同机器上的节点只要在同一网络或配置好 DDS 发现域就能组成同一张 ROS 图。1.3 节点、进程与线程初学者常混淆三个概念进程Process操作系统层面的运行实例。ROS2 中一个进程可以包含一个节点也可以包含多个节点通过 Composable Node / 组件机制。节点NodeROS 层面逻辑单元负责通信。一个进程里可以有多个节点。线程Thread进程内的执行流。rclpy/rclcpp 的spin默认在单线程里轮流执行回调。理解它们的层次进程包含节点节点运行在线程上。默认情况下每个节点在自己的进程里运行一个进程 一个节点这样崩溃隔离性最好但当节点很多、通信频繁时进程间通信开销大就可以把多个节点组合进一个进程共享内存这就是后面会讲的launch 与 Composable Node。二、用 Python 创建第一个节点2.1 最简 rclpy 节点先看一个只创建节点、不做任何通信的最简 Python 节点# minimal_node.py —— 最简 rclpy 节点importrclpyfromrclpy.nodeimportNodeclassMyNode(Node):def__init__(self):# 第一个参数是节点名称第二个参数是命名空间可省略super().__init__(my_python_node)self.get_logger().info(Python 节点已启动)defmain(argsNone):rclpy.init(argsargs)# 1. 初始化 rclpynodeMyNode()# 2. 创建节点实例rclpy.spin(node)# 3. 进入事件循环等待回调node.destroy_node()# 4. 清理节点rclpy.shutdown()# 5. 关闭 rclpyif__name____main__:main()运行方式# 直接运行前提已 source 环境变量且当前目录能找到该文件python3 minimal_node.py你会看到终端打印出类似这样的日志[INFO] [1690000000.123456789] [my_python_node]: Python 节点已启动由于rclpy.spin()会一直运行程序不会退出除非按 CtrlC。这个节点现在没有任何话题和服务但你可以在另一个终端用ros2 node list看到它。2.2 节点名称与命名空间节点名称在 ROS 图中必须唯一。命名空间Namespace用于给节点分组类似文件系统的目录结构。创建带命名空间的节点# namespace_node.py —— 演示命名空间importrclpyfromrclpy.nodeimportNodedefmain():rclpy.init()# 节点全名 命名空间 节点名即 /robot_arm/controllernodeNode(controller,namespace/robot_arm)node.get_logger().info(f节点全名:{node.get_name()})node.get_logger().info(f命名空间:{node.get_namespace()})node.get_logger().info(f带命名空间的全名:{node.get_fully_qualified_name()})rclpy.spin(node)node.destroy_node()rclpy.shutdown()if__name____main__:main()运行后ros2 node list中会显示/robot_arm/controller。注意节点全名fully qualified name 命名空间 节点名命名空间以/开头各级之间也用/分隔。命名空间的典型用途一台机器人上可能有左右两个机械臂用/left_arm和/right_arm两个命名空间隔离这样每侧内部的节点名可以相同而不冲突。2.3 定时器让节点活起来一个只会打印一次日志的节点太单调了。加入定时器Timer让节点周期性地执行任务# timer_node.py —— 定时器节点importrclpyfromrclpy.nodeimportNodeclassTimerNode(Node):def__init__(self):super().__init__(timer_node)self.count0# 每 1.0 秒触发一次回调注意单位是秒浮点数self.timerself.create_timer(1.0,self.timer_callback)deftimer_callback(self):self.count1self.get_logger().info(f第{self.count}次心跳)defmain():rclpy.init()nodeTimerNode()rclpy.spin(node)node.destroy_node()rclpy.shutdown()if__name____main__:main()运行后每秒钟打印一次心跳。定时器是 ROS2 节点最常用的节拍器后面所有周期发布如传感器数据、控制指令都建立在定时器之上。三、用 C 创建节点3.1 最简 rclcpp 节点C 版本的节点结构更偏向显式生命周期管理// minimal_node.cpp —— 最简 rclcpp 节点#includerclcpp/rclcpp.hppclassMyNode:publicrclcpp::Node{public:MyNode():Node(my_cpp_node){RCLCPP_INFO(this-get_logger(),C 节点已启动);}};intmain(intargc,char**argv){rclcpp::init(argc,argv);// 1. 初始化autonodestd::make_sharedMyNode();// 2. 创建节点智能指针rclcpp::spin(node);// 3. 事件循环rclcpp::shutdown();// 4. 关闭return0;}编译与运行在 package 环境中参考第 4 篇的 package 结构colcon build --packages-select demo_node_cppsourceinstall/setup.bash ros2 run demo_node_cpp minimal_node3.2 C 定时器节点对应 Python 的定时器示例// timer_node.cpp —— C 定时器节点#includerclcpp/rclcpp.hpp#includechronoclassTimerNode:publicrclcpp::Node{public:TimerNode():Node(timer_node_cpp),count_(0){// 1.0 秒周期单位是 std::chrono::durationtimer_this-create_wall_timer(std::chrono::seconds(1),std::bind(TimerNode::timer_callback,this));}private:voidtimer_callback(){count_;RCLCPP_INFO(this-get_logger(),第 %ld 次心跳,count_);}rclcpp::TimerBase::SharedPtr timer_;longcount_;};intmain(intargc,char**argv){rclcpp::init(argc,argv);rclcpp::spin(std::make_sharedTimerNode());rclcpp::shutdown();return0;}注意 C 的几个细节create_wall_timer的时间参数是std::chrono::duration比 Python 的秒数更精确。回调函数需要std::bind绑定到成员函数或使用 lambda 捕获this。定时器和回调都要保存为成员变量timer_防止被垃圾回收/析构。3.3 Python 与 C 节点对比维度Python (rclpy)C (rclcpp)初始化rclpy.init()rclcpp::init(argc, argv)创建节点Node(name)std::make_sharedMyNode()或Node(name)事件循环rclpy.spin(node)rclcpp::spin(node)日志self.get_logger().info()RCLCPP_INFO(this-get_logger(), ...)定时器create_timer(1.0, cb)create_wall_timer(std::chrono::seconds(1), cb)生命周期自动引用计数手动init/shutdown 智能指针适用场景原型验证、脚本、快速迭代性能敏感、实时性要求高、生产部署经验法则学概念用 Python 快上真机、做控制用 C 稳。两者在 API 设计上高度对称掌握了其中一个另一个几乎是翻译关系。四、节点生命周期与执行模型4.1 生命周期五阶段一个 ROS2 节点从出生到销毁经历清晰的阶段下图即执行流初始化(init) → 构造(创建Node实例) → 配置(声明参数/创建端点) → 运行(spin事件循环) → 销毁(destroy/shutdown)初始化rclpy.init()/rclcpp::init()启动底层 DDS、解析命令行参数如--ros-args。构造调用构造函数此时创建定时器、话题、服务等一切端点。运行spin()阻塞式地轮询回调队列有事件就执行没事件就挂起。注意构造期间回调不会执行所以不要在构造函数里做依赖回调结果的逻辑。销毁destroy_node()shutdown()释放 DDS 资源、断开连接。程序退出前一定要走完这一步否则可能出现僵尸连接或端口占用。4.2 spin 的本质回调循环理解spin是理解 ROS2 执行模型的关键。spin做的事情可以简化成while (节点存活) { 从回调队列取出一个待执行的回调; // 可能来自话题消息、服务请求、定时器触发 执行该回调; // 默认单线程一次只执行一个 }这意味着回调的执行是串行的。如果某个回调里写了sleep(10)或死循环其他回调包括定时器都会卡住。这是一切节点不响应了问题的根源。因此回调里绝不能做耗时操作文件 IO、网络请求、复杂计算耗时任务应放子线程或用MultiThreadedExecutor多线程执行器。4.3 多线程执行器Python 示例C 的rclcpp::executors::MultiThreadedExecutor思路相同# multi_thread.py —— 多线程执行器importrclpyimportthreadingfromrclpy.nodeimportNodefromrclpy.executorsimportMultiThreadedExecutorclassBusyNode(Node):def__init__(self):super().__init__(busy_node)self.timer_aself.create_timer(0.5,self.cb_a)self.timer_bself.create_timer(0.5,self.cb_b)defcb_a(self):# 模拟耗时 3 秒的阻塞操作importtime;time.sleep(3)self.get_logger().info(A 完成)defcb_b(self):self.get_logger().info(B 执行)defmain():rclpy.init()nodeBusyNode()executorMultiThreadedExecutor(num_threads4)# 4 线程executor.add_node(node)# 在子线程中运行执行器tthreading.Thread(targetexecutor.spin,daemonTrue)t.start()try:importtimewhileTrue:time.sleep(1)exceptKeyboardInterrupt:executor.shutdown()rclpy.shutdown()if__name____main__:main()对比观察用SingleThreadedExecutor时B 会被 A 的 3 秒阻塞卡住用MultiThreadedExecutor后B 在另一个线程执行、正常响应。多线程执行器是让一个节点既能算又能响应的标准解法但要注意线程安全问题回调间共享数据要加锁。五、节点参数与日志5.1 声明与读取参数节点参数Parameter是节点的配置项运行时可动态修改。Python 声明参数# param_node.py —— 参数演示Pythonimportrclpyfromrclpy.nodeimportNodefromrclpy.parameterimportParameterclassParamNode(Node):def__init__(self):super().__init__(param_node)# 声明参数名称 默认值 描述self.declare_parameter(robot_name,turtle_01)self.declare_parameter(max_speed,2.5)self.declare_parameter(verbose,True)# 读取参数返回 Parameter 对象用 .value 取值self.robot_nameself.get_parameter(robot_name).value self.max_speedself.get_parameter(max_speed).value self.get_logger().info(f机器人:{self.robot_name}, 最大速度:{self.max_speed})defmain():rclpy.init()nodeParamNode()rclpy.spin(node)node.destroy_node()rclpy.shutdown()if__name____main__:main()命令行设置参数后运行ros2 runpkgparam_node --ros-args-probot_name:my_rover-pmax_speed:3.05.2 C 参数对照// param_node.cpp —— 参数演示C#includerclcpp/rclcpp.hppclassParamNode:publicrclcpp::Node{public:ParamNode():Node(param_node_cpp){this-declare_parameter(robot_name,turtle_01);this-declare_parameter(max_speed,2.5);autonamethis-get_parameter(robot_name).as_string();autospeedthis-get_parameter(max_speed).as_double();RCLCPP_INFO(this-get_logger(),机器人: %s, 最大速度: %.1f,name.c_str(),speed);}};intmain(intargc,char**argv){rclcpp::init(argc,argv);rclcpp::spin(std::make_sharedParamNode());rclcpp::shutdown();return0;}C 读取参数后要用as_string()、as_double()等方法转成具体类型。参数系统是配置与代码分离的基石同一份代码通过不同参数启动就能适配不同机器人。5.3 日志级别与过滤ROS2 日志分五个级别从低到高DEBUG INFO WARN ERROR FATAL。默认只输出 INFO 及以上的日志。在终端设置日志级别ros2 runpkgtimer_node --ros-args --log-level DEBUGPython 中控制日志# 在节点内输出各级日志node.get_logger().debug(调试信息默认不显示)node.get_logger().info(普通信息)node.get_logger().warn(警告信息)node.get_logger().error(错误信息)node.get_logger().fatal(致命错误)实践建议开发期用 DEBUG 记录详细流程发布前降回 INFO不要用print代替get_logger——日志带时间戳、节点名、级别还能统一过滤和重定向到 ros2 bag这是 print 做不到的。六、实战多节点协作小项目6.1 项目结构下面组织一个心跳-监视双节点项目heartbeat_node每秒发布心跳次数monitor_node每 3 秒查询一次用参数传递心跳值。为演示方便这里用两个 Python 文件 一个 launch 文件。multi_node_demo/ ├── heartbeat.py # 心跳节点定时器 参数 ├── monitor.py # 监视节点定时器读取参数 └── start.launch.py # launch 文件同时启动两个节点6.2 心跳节点# heartbeat.py —— 心跳节点importrclpyfromrclpy.nodeimportNodeclassHeartbeatNode(Node):def__init__(self):super().__init__(heartbeat)self.declare_parameter(rate,1.0)# 心跳频率参数self.declare_parameter(heartbeat,0)# 心跳计数共享用self.count0rateself.get_parameter(rate).value self.timerself.create_timer(rate,self.beat)defbeat(self):self.count1# 把计数写回参数同进程内其他节点可读简单演示用self.set_parameters([rclpy.parameter.Parameter(heartbeat,rclpy.parameter.Parameter.Type.INTEGER,self.count)])self.get_logger().info(f心跳 #{self.count})defmain():rclpy.init()nodeHeartbeatNode()rclpy.spin(node)node.destroy_node()rclpy.shutdown()if__name____main__:main()6.3 监视节点# monitor.py —— 监视节点importrclpyfromrclpy.nodeimportNodeclassMonitorNode(Node):def__init__(self):super().__init__(monitor)self.declare_parameter(heartbeat,0)# 每 3 秒检查一次心跳值self.timerself.create_timer(3.0,self.check)defcheck(self):hbself.get_parameter(heartbeat).value self.get_logger().info(f当前心跳计数:{hb})defmain():rclpy.init()nodeMonitorNode()rclpy.spin(node)node.destroy_node()rclpy.shutdown()if__name____main__:main()说明真实项目中节点间共享数据应该用话题第 5 篇详讲这里用参数只是演示参数可在进程内共享的机制。多进程间参数不共享正式通信请用话题/服务。6.4 launch 文件同时启动# start.launch.py —— 同时启动心跳与监视节点fromlaunchimportLaunchDescriptionfromlaunch_ros.actionsimportNodedefgenerate_launch_description():returnLaunchDescription([Node(packagedemo_nodes_py,executableheartbeat,nameheartbeat),Node(packagedemo_nodes_py,executablemonitor,namemonitor),])运行# 把 heartbeat.py / monitor.py 放入 demo_nodes_py 包并 colcon build 后ros2 launch multi_node_demo start.launch.py# 另开终端查看节点ros2nodelist6.5 ros2 node 指令全览日常排障最常用的节点相关命令ros2nodelist# 列出所有节点ros2nodeinfo /heartbeat# 查看节点详细信息话题/服务/参数/动作ros2nodelist--verbose# 带类型信息列出ros2 param list /heartbeat# 查看节点参数ros2 param get /heartbeat rate# 获取单个参数值ros2 paramset/heartbeat rate2.0# 运行时修改参数其中ros2 node info 节点全名是最重要的诊断命令它会列出该节点发布/订阅的话题、提供的服务、声明的参数等排障时第一件事就是看它。七、总结本篇文章建立了 ROS2 的最小世界观节点是功能单元ROS 图是节点组成的协作网络通信端点话题/服务/参数/动作是节点间的连接方式。我们分别用 Python 与 C 创建了最简节点、定时器节点理解了spin回调循环的本质与多线程执行器最后用一个心跳-监视双节点项目串起了全部知识。关键要点回顾节点全名 命名空间 节点名全局唯一。spin()是单线程串行执行回调耗时操作会阻塞其他回调。定时器让节点周期工作是传感器发布、控制循环的节拍器。参数实现配置与代码分离运行时可动态修改。用get_logger()而非print用ros2 node info诊断节点状态。ROS2 无中心节点节点经 DDS 自动发现组成 ROS 图天然支持多机分布式。下一篇预告下一篇我们搭建工作空间Workspace与 colcon 构建体系理解 src/build/install/log 四目录结构学会创建自定义 package、配置 CMakeLists.txt 与 package.xml并掌握colcon build的常用选项--packages-select、--symlink-install、--cmake-args等。届时你的每一个节点都能用ros2 run正式运行为后续话题、服务等通信篇目打好工程基础。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →