尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零创建ROS2 Python节点:rclpy完整实践指南

发布时间:2026/9/25 4:38:19

资讯中心
01
ARTICLE

从零创建ROS2 Python节点:rclpy完整实践指南

从零创建ROS2 Python节点:rclpy完整实践指南
不管你是刚从ROS1迁移过来还是完全零基础直接上手ROS2第一个拦路虎基本都是同一个——怎么写一个能跑起来的节点。网上关于rclpy的教程不少但要么是官方文档的翻译腔要么就是贴一段代码然后说“这样就跑起来了”至于代码背后的执行逻辑、为什么这么写、踩了坑怎么排查往往没人讲透。这篇文章就以“创建Python类型节点”为切入点把rclpy的完整用法拆开揉碎从环境准备、工程组织、代码编写到编译运行和排错全部过一遍。适合刚入门ROS2的开发者也适合那些写过节点但总觉得“哪里没搞懂”的朋友。1. 内容整体设计与思路拆解1.1 为什么选Python而不是C先回答一个很多人纠结的问题ROS2节点用Python写还是C写我的建议是如果你不是对实时性有硬性要求或者不是在搞底层驱动、硬件控制这类对性能敏感的东西Python的rclpy足够应付绝大多数场景。ROS2的通信架构里Python客户端库和C客户端库走的都是同一套DDS中间件话题、服务、动作这些通信机制没有本质区别。你用Python写一个发布器C的订阅器照样能收到消息反之亦然。区别主要体现在性能和开发效率的权衡上。rclpy在消息序列化、回调执行这些环节确实比rclcpp慢一些但换来的是极其舒服的开发体验不用写CMakeLists编译、不用处理头文件依赖、改完代码直接rerun就行。我在实际项目里做过粗略对比一个每秒发布10次的控制指令节点Python版CPU占用不到1%这个开销在大多数场景下根本感知不到。所以别纠结语言选型先把节点逻辑跑通后续真有性能瓶颈再拿C重写关键路径也不迟。而且Python节点的代码结构、节点生命周期管理的方式和C是高度对应的学会一个迁移到另一个成本很低。1.2 ROS2节点的工作原理简析在动手写代码之前得先理解“节点”在ROS2里到底是什么。一句话概括节点就是一个独立运行的进程它通过DDS与系统中的其他节点通信。这和ROS1有本质区别——ROS1需要roscore作为中心调度节点之间通过master间接通信ROS2去掉了中心节点每个节点直接通过DDS的发现机制互相找到对方所以天生支持多机分布式部署。rclpy就是ROS2提供给Python开发者的客户端库。它的作用可以理解成一个“翻译层”你的Python代码通过rclpy的API创建节点、发布话题、订阅消息底层由C库rcl和DDS实现来真正干活。这也是为什么你要import rclpy而不是直接去操作DDS——大多数情况下你根本不需要关心底层细节。理解这个分层之后很多疑问就迎刃而解了。比如为什么节点要rclpy.init()之后才能创建因为rclpy.init()做的事情是初始化整个客户端库的运行环境包括通信层的全局上下文为什么spin()会让程序一直阻塞因为spin的本质是让节点持续处理传入的回调事件没有spin订阅消息来了也没人处理。1.3 版本选择与环境准备ROS2的发行版比较多目前主流的编程环境基本都集中在两个Ubuntu 22.04上装HumbleUbuntu 24.04上装Jazzy。如果你用的是Windows或macOS虽然官方支持但坑比较多这里不推荐拿来学习。我自己在Humble和Jazzy两个版本下都验证过本文的代码语法层面没有差异。装好ROS2之后先确认环境变量有没有生效printenv | grep ROS_DISTRO如果输出是humble或jazzy说明环境加载成功。还没source环境的话记得手动加载source /opt/ros/humble/setup.bash然后检查Python端是否正常python3 -c import rclpy; print(rclpy.__version__)能打印出版本号就说明rclpy可用。这里有一个新手特别容易踩的坑ROS2的Python库是绑定在系统Python环境里的千万不要用pip install rclpy去覆盖也不要用虚拟环境venv去跑ROS2的Python代码否则会各种import失败、版本冲突。2. 核心细节解析与实操要点2.1 工作空间是什么以及怎么建ROS2的工程组织单位是“工作空间workspace”最经典的结构是src目录下放各个功能包。创建方式很简单mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src很多教程让你直接用命令生成包但我觉得有必要先说清楚目录结构的意义——这会直接影响你以后排查问题的能力。一个标准的Python功能包包含以下几个关键文件my_package/ ├── package.xml # 包的元信息和依赖声明 ├── setup.py # Python安装配置 ├── setup.cfg # 让ROS2能找到可执行脚本 ├── resource/ # 标记这是一个ROS2包 └── my_package/ # 实际的Python代码目录 └── __init__.py目录结构看懂之后你会发现ROS2的包管理其实并没有想象中复杂package.xml告诉构建系统这个包依赖谁setup.py告诉安装系统Python代码怎么装setup.cfg告诉ros2 run去哪里找可执行文件。这三者缺一不可。2.2 用ros2 pkg create一键生成包手动建目录当然可以但更推荐用官方命令自动生成。这样能保证目录结构不出错省去很多低级问题cd ~/ros2_ws/src ros2 pkg create --build-type ament_python learning_nodes--build-type ament_python指定了这是Python类型的包生成成功后你会看到learning_nodes目录下已经有了package.xml、setup.py、setup.cfg这些文件。别忘了同步安装依赖工具sudo apt install python3-colcon-common-extensions2.3 package.xml、setup.py、setup.cfg逐个吃透生成的包只是模板里面的依赖和配置基本都是空的。我见过太多人在这三个文件上翻车咱们逐个过一遍。先看package.xml。对于Python包核心依赖就两类。build_depend只在构建时需要而exec_depend是运行时的硬依赖。这里要明确加上rclpybuildtool_dependament_python/buildtool_depend exec_dependrclpy/exec_depend再看setup.py。除了包名、版本号、描述这些基本信息最关键的入口是entry_points。它声明了“当用户执行ros2 run learning_nodes simple_node时实际去运行哪个函数”entry_points{ console_scripts: [ simple_node learning_nodes.simple_node:main, ], },注意这个格式左侧是命令名右侧是模块路径:函数名中间用空格加等号分隔。写错任何一个字符ros2 run都会报Package learning_nodes not found或者executable not found。最后是setup.cfg生成模板基本不用改但你要知道它存在的意义[develop] script_dir$base/lib/learning_nodes [install] install_scripts$base/lib/learning_nodes它告诉colcon安装后把可执行脚本放到lib/learning_nodes目录下。如果你瞎改这个路径即使包编译成功ros2 run也找不到命令。3. 实操过程与核心环节实现3.1 第一个最小节点从init到spin现在进入正题。在learning_nodes/learning_nodes/目录下创建simple_node.py这是你的第一个最小可运行节点import rclpy from rclpy.node import Node class SimpleNode(Node): def __init__(self): super().__init__(simple_node) self.get_logger().info(节点已启动) def main(): rclpy.init() node SimpleNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown() if __name__ __main__: main()代码只有十几行但每一行都有讲究。rclpy.init()负责初始化客户端库。如果你忘了写创建节点时会直接抛rclpy._rclpy_pybind11.RCLError: failed to initialize之类的异常。SimpleNode继承自Nodesuper().__init__(simple_node)里的字符串就是节点名。get_logger()返回一个日志对象等价于ROS1里的ROS_INFO。最不能省的是rclpy.spin(node)。spin的英文原意是“旋转”你可以理解成进入一个死循环不断处理节点收到的各种事件。没有spin你的节点进程会直接跑完main函数退出。节点名为什么在这里要单独拎出来说因为ROS2的节点名在分布式系统里相当于进程的身份证不能重复而且名字里不能有空格和特殊字符。3.2 千万别让节点被垃圾回收上面的代码里有一个暗坑如果写成下面这样节点会瞬间消失def main(): rclpy.init() rclpy.spin(SimpleNode()) rclpy.shutdown()SimpleNode()这个临时对象没有赋值给变量Python的垃圾回收机制会在函数作用域里立刻回收它。节点被销毁spin自然就退出了。你在终端里会看到节点名一闪而过ros2 node list里什么都查不到。正确的做法是让节点对象活着直到spin结束node SimpleNode() rclpy.spin(node)3.3 让节点动起来添加定时器和计数器一个只会打印日志的节点没有实际意义接下来给它加一个定时器任务。比如每秒发布一个递增计数模拟传感器周期性发数据的场景import rclpy from rclpy.node import Node from std_msgs.msg import Int32 class CounterNode(Node): def __init__(self): super().__init__(counter_node) self.publisher self.create_publisher(Int32, counter, 10) self.count 0 self.timer self.create_timer(1.0, self.timer_callback) def timer_callback(self): msg Int32() msg.data self.count self.publisher.publish(msg) self.get_logger().info(f发布计数: {self.count}) self.count 1 def main(): rclpy.init() node CounterNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown()create_publisher有三个参数消息类型、话题名、队列深度。create_timer的第一个参数是时间间隔秒第二个参数是回调函数。这里有几个细节值得注意。队列深度参数10代表消息队列能缓存10条消息。如果订阅端处理不过来旧消息会被丢弃。这个值不是越大越好内存占用和时效性需要平衡。Int32().data必须显式赋值。很多人直接用msg Int32(dataself.count)其实也行但要知道std_msgs里的消息都是这样带data字段的简单结构。消息类型不匹配是话题通信最常见的错误比如发布方用Int32订阅方声明Float32两端都不会报错但消息永远收不到。3.4 发布订阅让两个节点对话单节点自嗨没意思我们再写一个订阅节点让两个节点真正对话起来。创建subscriber_node.pyimport rclpy from rclpy.node import Node from std_msgs.msg import Int32 class SubscriberNode(Node): def __init__(self): super().__init__(subscriber_node) self.subscription self.create_subscription( Int32, counter, self.listener_callback, 10) def listener_callback(self, msg): self.get_logger().info(f收到计数: {msg.data}) def main(): rclpy.init() node SubscriberNode() rclpy.spin(node) node.destroy_node() rclpy.shutdown()注意create_subscription的参数顺序和create_publisher不一样发布是(类型, 话题, 回调是create_publisher的第三个参数里的callback吗回头看——create_publisher(Int32, counter, 10)第三个参数是队列深度create_subscription(Int32, counter, self.listener_callback, 10)第三个参数是回调函数队列深度在第四位。这个不对称让很多人写错过。还有一个高频问题订阅节点必须也在spin状态下才能收到消息。因为消息到来时回调函数是在spin的循环里被调用的。你写好了回调不调用spin回调永远不会执行。3.5 代码之外的配置改三处才能编译运行写完两个Python文件要跑起来还需要改三个地方。去setup.py里把入口函数都声明上entry_points{ console_scripts: [ simple_node learning_nodes.simple_node:main, counter_node learning_nodes.counter_node:main, subscriber_node learning_nodes.subscriber_node:main, ], },然后确认package.xml里已经写上exec_dependrclpy/exec_depend和exec_dependstd_msgs/exec_depend。很多人漏掉std_msgs结果编译没问题运行时import报错。接着编译。在~/ros2_ws目录下执行colcon build --packages-select learning_nodes--packages-select可以只编译指定的包在大工作空间里能省大量时间。编译完成后必须source一下才能让系统找到新包source install/setup.bashsource这步太容易被忽略了。刚编译完直接ros2 run大概率会提示找不到包但你明明已经编译成功——先检查有没有source环境。这个操作在新终端里都要重新执行不想每次手敲的可以写进~/.bashrc。3.6 运行验证从ros2 run到ros2 node list终于到运行环节。开三个终端分别执行ros2 run learning_nodes simple_node ros2 run learning_nodes counter_node ros2 run learning_nodes subscriber_nodecounter节点启动后subscriber终端会开始刷“收到计数: x”证明整个通信链路是通的。再用一组ROS2自带命令来确认结构ros2 node list ros2 node info /counter_node ros2 topic list ros2 topic echo /counterros2 node info输出里会看到该节点发布的/counter话题。这套命令配合日常调试非常顺手比在代码里到处打日志高效得多。尤其是ros2 topic echo可以直接在终端里查看话题上流动的实时数据排查通信问题必备。4. 常见问题与排查技巧实录4.1 import rclpy失败的几种原因这是新手报错频率最高的问题。第一种情况是根本没装ROS2那不用说了先装好环境。第二种情况是ROS2装了但环境没sourcepython3 -c import rclpy会报ModuleNotFoundError。第三种情况比较隐蔽——用了conda或venv虚拟环境。conda默认会切换到自己的Python路径把你隔离在系统环境之外ROS2的包自然是找不到的。如果你开了conda环境先conda deactivate再跑。还有一个很多人问的问题能不能用pip install rclpy不建议。ROS2的rclpy是绑着特定发行版走的和系统里的rcl、rmw实现强相关。混装版本极易引发运行时崩溃报错还特别难查。4.2 colcon build相关的坑编译阶段最常见的报错是Package learning_nodes not found不是编译报错是运行时报的多半是忘了source。另一个是ros2 pkg create生成的模板里package.xml没有正确填写description和maintainercolcon build会报错让你补全。把这两个字段写上注意maintainer里必须有email属性。还有的报错长这样ModuleNotFoundError: No module named learning_nodes这通常意味着setup.py里的packages字段没有正确声明。生成的模板默认是这样packages[learning_nodes]如果你把代码放在了子目录里没注册import必然失败。保持生成的目录结构不要乱动就不会有这个问题。4.3 ros2 run找不到节点的排查ros2 run learning_nodes counter_node报executable not found基本就是setup.py的entry_points写错了。中文输入法经常把冒号打成全角或者把写成中文全角空格这都够你排查半天。我的排查习惯是三步走先看setup.py里console_scripts的语法有没有问题再去install/learning_nodes/lib/learning_nodes/目录下看有没有生成对应的可执行脚本最后检查setup.cfg的script_dir路径有没有被改动。4.4 节点启动秒退的深度排查如果你运行节点后终端没有任何输出就退出了先用最笨的办法排查在main函数里加一行print(start)看打印不打印。如果不打印说明Python解释器都没跑到你的代码优先检查import语句是不是挂了。如果打印了说明卡在rclpy.init()或spin上。另一个秒退原因是端口冲突或DDS发现失效。ROS2底层走的DDS默认使用UDP端口在多机场景或某些网络环境下会有问题。这时候看环境变量有没有ROS_DOMAIN_ID——同一网段内如果不同设备的domain id不一致节点之间就“老死不相往来”表现就是单个节点启动正常但话题收不到数据、node list互相看不到。简单说所有互联的机器必须设置同一个ROS_DOMAIN_ID。排查命令就这么一行echo $ROS_DOMAIN_ID没设置默认都是0两边一致才行。4.5 话题通了但收不到数据的进阶排查ros2 topic list能看到话题但topic echo没有任何输出这是最让人头大的问题。优先怀疑消息类型不匹配。发布方和订阅方用的消息类型必须完全一致std_msgs/Int32和std_msgs/Float32看似差不多其实在DDS层会被识别成两个完全不同的话题类型。其次检查通信两端是不是在同一个网络域。前面说了ROS_DOMAIN_ID不一致会造成“老死不相往来”。最后检查是否使用了不同的ROS_DOMAIN_ID或者存在防火墙拦截UDP多播的情况这个在实体机器人部署时经常遇到。5. 从Demo到工程化多文件与参数配置5.1 多节点拆分的工程哲学看完上面的例子你可能想说就这发布订阅我在教程里看过无数次了。但如果只是停留在跑通Demo技术深度远远不够。下面聊聊怎么把单节点Demo改造成真正的工程。工程化的第一件事是拆分文件。一个节点的代码不应该都堆在main()里。比如你的机器人节点需要同时处理传感器数据、运动控制、状态上报全塞一个Python文件几百行起步后期维护成本极高。合理的做法是拆成多个模块learning_nodes/ ├── __init__.py ├── simple_node.py ├── counter_node.py ├── subscriber_node.py └── config.py各个节点在各自文件里独立实现config.py放共享常量。这在团队协作时特别有用——A负责传感器节点B负责控制节点互不干扰。5.2 给节点加参数不做“硬编码狂魔”Demo里把话题名、发布频率直接写在代码里这在真实项目中不行。ROS2提供了参数机制让节点可以在启动时动态配置。在构造函数里声明参数self.declare_parameter(topic_name, counter) self.declare_parameter(publish_rate, 1.0) topic_name self.get_parameter(topic_name).get_parameter_value().string_value rate self.get_parameter(publish_rate).get_parameter_value().double_value启动时这样传参ros2 run learning_nodes counter_node --ros-args -p topic_name:my_counter -p publish_rate:2.0这比改代码重编译高效太多。尤其在调参阶段不用每次改完代码重新编译source参数机制的体验我认为是ROS2相比ROS1一个很大的进步。5.3 从单节点走向多节点系统的一点经验文章最后分享一条我在实际项目中摸爬滚打得到的经验学会用ros2 launch管理多个节点。三个终端各跑一个ros2 run只是入门方式真实项目的节点数量往往在十几个以上手动启动注定是一场灾难。launch文件可以把所有节点的启动配置固化在一个文件里from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node(packagelearning_nodes, executablecounter_node, namecounter_node), Node(packagelearning_nodes, executablesubscriber_node, namesubscriber_node), ])一个命令全部拉起ros2 launch learning_nodes demo.launch.pynode的name参数还可以覆盖代码里写的节点名这在多机部署时特别有用。另外还有一个容易被忽视的小建议养成用ros2 bag record -a记录话题数据的习惯。真实机器人调试时同一段问题可能要反复回放数据才能定位有了录制好的bag文件你可以在自己的电脑上离线重现现场分析效率翻倍。回到最初的问题——创建Python节点这件事本身并没有多难难的是把通信机制、生命周期、工程组织这些环节串起来。我见过太多人在第一步就被各种环境问题劝退或者跑通一个Demo就觉得ROS2不过如此。其实上手ROS2最正确的姿势恰恰是先从最简单的Python节点开始把发布订阅、生命周期、参数系统这些基础概念吃透再往launch、tf、nav2这些上层框架走整体学习曲线会比直接啃C节点平滑得多。而且rclpy的API设计总体上相当一致当你掌握了一个节点的完整生命周期后面写十个节点也只是复制熟练而已。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。