YAOTU INSIGHTS

ROS Publisher编写实战:从工作空间到话题通信的完整流程

ROS Publisher编写实战:从工作空间到话题通信的完整流程
写第一个ROS Publisher之前我一直以为难点在写代码。等真正跑通一个话题通信之后才发现代码是最不值钱的部分真正卡住新手的是节点、话题、消息到底怎么协作这个底层画面没建立起来。这篇笔记我用最直白的方式记录一次完整的Publisher编写过程——从工作空间搭建、C和Python两个版本的实现到编译运行、用命令行验证数据流向再到我实际踩过的几个坑。适合刚装好ROS、准备动手写第一个节点的朋友也适合已经能跑例程但对话题机制还是似懂非懂的初学者。1. 动手写代码前先把话题通信这件事想明白很多教程上来就是ros::Publisher、advertise()、publish()一顿操作代码抄完了换个场景还是不会写。我自己的体会是Publisher只是整个通信链路里的一环你如果不理解这条链路写出来的节点要么跑不起来要么跑起来完全不知道数据去哪了。1.1 ROS节点通信的本质广播而不是打电话ROS里两个节点之间传递数据用的不是A直接调B的接口这种打电话模式而是广播电台模式。Publisher就是那个电台主播它往一个固定频率上持续输出内容它根本不知道有没有人在听。Subscriber是听众它调好频道就能收到内容它也不用关心主播在哪、长什么样。这个设计的好处是解耦发布者和订阅者之间没有直接依赖关系。你可以先启动发布者监听者没启动也无所谓数据照样发布也可以只开一个订阅者等数据发布者晚点再来也行。两个节点甚至可以在不同电脑上运行只要它们连接同一个ROS Master名字服务器就能自动发现彼此。我第一次理解这个模型的时候脑袋里蹦出来的画面是Publisher像小区广播喇叭每天固定时间播报Subscriber像住户到点打开收音机就行。喇叭坏了不影响住户生活住户不听喇叭也照样活着——这就是ROS节点之间最核心的松耦合关系。1.2 话题Topic、消息Message、节点Node的分工节点Node一个可执行程序负责干活比如读传感器、算路径、控制电机。话题Topic节点之间传数据的频道名称比如/cmd_vel、/odom、/image_raw。消息Message在话题上传输的数据格式比如geometry_msgs/Twist、sensor_msgs/Image。消息定义了数据长什么样包含哪些字段。Publisher干的事就是往指定话题上按指定消息类型以指定频率发布数据。就这么简单。但这句话里每个指定都需要你之前规划清楚——这和写C函数不一样你得有一个全局视角知道谁需要这个数据、数据长什么样、多久发一次。1.3 自己写的Publisher到底负责什么以写一个发布速度指令的节点为例它负责三件事初始化节点告诉ROS我叫什么名字。声明我要往/cmd_vel话题发布geometry_msgs/Twist类型的数据。循环里不停计算新数据比如按时间变化的线速度和角速度然后发布出去。你不需要管数据到了网络上怎么封包、Subscriber在哪台机器、底层用TCP还是UDP——这些都是ROS通信框架roscpp/rospy替你处理好的。这也是初学者最容易纠结的地方我到底要不要懂底层我的答案是第一周不用先跑通再回头补。2. 环境准备与工作空间搭建少走弯路的关键一步写Publisher之前必须先有一个能编译、能运行的ROS环境还要把工作空间和功能包建好。这一步看起来繁琐但值得认真过一遍因为后续所有节点都在这个框架里跑。如果你用的是一键脚本装的ROS比如鱼香ROS我自己用过确实省事那ROS本体就齐了接下来要解决的是你自己代码的存放和编译问题。2.1 确认ROS版本和环境变量不同ROS版本的命令有细微差别写Publisher之前最好先确认一下echo $ROS_DISTRO我用的Ubuntu 20.04配ROS Noetic如果你装的是ROS 2Humble/Foxy命令和代码风格会差很多。本文以ROS 1为主但核心思想在ROS 2里同样适用只是advertise变成了create_publisherMaster变成了DDS发现机制。环境变量这个坑太经典了。source /opt/ros/noetic/setup.bash这条命令每次打开新终端都要跑或者写进~/.bashrc。我见过太多人明明ROS装好了一运行roscore就说command not found十有八九是没source。2.2 创建ROS工作空间catkin工作空间工作空间是存放、编译、运行ROS项目的目录推荐结构如下mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspace cd ~/catkin_ws catkin_make source devel/setup.bash解释一下每条命令在干嘛catkin_init_workspace把src目录初始化成catkin工作空间生成一个CMakeLists.txt符号链接。catkin_make编译整个工作空间生成build编译中间文件和devel编译产物、环境脚本两个目录。source devel/setup.bash让当前终端认识这个工作空间里的节点和库。这一步忘了后面rosrun找不到你的节点。我自己建工作空间有个习惯catkin_make之后先不急着写代码立刻source一次跑一下echo $ROS_PACKAGE_PATH确认工作空间被识别。如果能正常返回路径列表说明环境OK后面写代码就不会被找不到包这种问题反复折磨。2.3 创建一个Publisher功能包功能包Package是ROS代码的基本组织单元包含代码、配置文件、编译规则等。创建发布者功能包的命令cd ~/catkin_ws/src catkin_create_pkg publisher_tutorial roscpp rospy std_msgs geometry_msgscatkin_create_pkg后面第一个参数是包名后面跟着的是这个包依赖的其他包。为什么要声明依赖因为ROS编译系统要帮你找头文件、链接库如果你不告诉它需要roscpp编译C节点的时候#include ros/ros.h会直接报错。我常用的依赖声明是roscpp rospy std_msgs这次还要发速度指令所以加上geometry_msgs。新手容易漏掉后边的依赖声明我建议是在创建包之前就想好要用哪些消息类型一次写全比后续在CMakeLists.txt里手工补要省事太多。创建完成后进到包里看一眼结构cd publisher_tutorial ls -l你会看到CMakeLists.txt、package.xml、include和src目录C、scripts目录Python可选。这些文件后面都要手动改尤其是CMakeLists.txt。2.4 一个容易被忽略的步骤package.xml的维护package.xml是功能包的身份证记录了包名、版本、依赖项、维护者等信息。catkin_create_pkg已经帮你把依赖写进去了但format文档里有一行build_depend和exec_depend有些版本需要确认是否都有geometry_msgs。规则很简单编译时需要什么写进build_depend运行时需要什么写进exec_depend或者直接用depend同时覆盖两种情况。dependroscpp/depend dependrospy/depend dependstd_msgs/depend dependgeometry_msgs/depend我习惯把依赖统一写成depend因为它会自动套用到编译和运行两种场景少操心。手动添加完依赖后记得回到工作空间再catkin_make一次确认XML没有语法问题。3. 用C写一个最朴素的Publisher节点C版本的Publisher是ROS学习路上最经典的例子没有之一。代码不长但每一行都有讲究我拆开讲。3.1 完整代码发布速度指令的talker节点在src目录下建一个velocity_publisher.cpp#include ros/ros.h #include geometry_msgs/Twist.h #include stdlib.h int main(int argc, char **argv) { // 1. 初始化ROS节点 ros::init(argc, argv, velocity_publisher); ros::NodeHandle n; // 2. 创建发布者对象 ros::Publisher cmd_vel_pub n.advertisegeometry_msgs::Twist(/cmd_vel, 100); // 3. 设置发布频率 ros::Rate loop_rate(10); while (ros::ok()) { // 4. 构造消息 geometry_msgs::Twist vel_msg; vel_msg.linear.x 0.5; vel_msg.linear.y 0; vel_msg.linear.z 0; vel_msg.angular.x 0; vel_msg.angular.y 0; vel_msg.angular.z 0.2; // 5. 发布消息 cmd_vel_pub.publish(vel_msg); ROS_INFO(Publishing: linear.x %.2f, angular.z %.2f, vel_msg.linear.x, vel_msg.angular.z); // 6. 按设定频率休眠 loop_rate.sleep(); } return 0; }3.2 每一段代码为什么这样写第1步初始化节点。ros::init(argc, argv, velocity_publisher)里的第三个参数是节点的名字这个名字必须在运行的ROS网络中唯一。如果你同时跑了两个同名节点ROS会强制给后启动的改名后面加数字这不算报错但说明你对命名规划不够细心。第2步创建NodeHandle和Publisher。NodeHandle是roscpp里跟ROS打交道的大门它管理节点的句柄、参数等资源。n.advertisegeometry_msgs::Twist(/cmd_vel, 100)做了两件事告诉ROS我要往/cmd_vel这个topic发数据数据类型是geometry_msgs::Twist第二件事是设置发送缓冲区大小100条。这里的100不是说每秒发100条而是指如果Subscriber来不及处理ROS会为它缓冲最近100条消息超过就丢弃旧消息。新手容易把这个buffer和频率混淆。第3步设置频率。ros::Rate loop_rate(10)表示循环以10Hz运行即每秒发布10条消息。它是怎么做到的loop_rate.sleep()会根据每次循环实际消耗的时间自动算出还需要睡多久从而保证发布周期稳定。这个设计比你手动usleep(100000)强得多因为代码运行时间不固定手动sleep会导致发布频率漂移。第4步构造消息。geometry_msgs::Twist由两个Vector3组成linear线速度和angular角速度。我们通常用linear.x表示前进速度angular.z表示旋转角速度。为什么要这么约定是因为机器人底盘驱动节点比如你买的底盘驱动包就是按这个约定去解析/cmd_vel的。你自己写发布者的时候一定要查询你底层驱动使用的消息类型和字段含义不能想当然。第5步publish做的事情本质上是把消息对象序列化后塞进话题队列ROS的IO线程会负责实际网络传输publish()本身不阻塞你尽可以理解成把信投进邮筒不需要等收件人回话。第6步ros::ok()的判断是为了能优雅地退出循环。CtrlC时ros::ok()会返回false程序能正常跳出循环、走析构清理资源。如果你写成while (true)CtrlC可能直接崩溃或者留下孤儿节点。3.3 CMakeLists.txt的配置90%的人第一次编译失败都在这C代码写完后必须告诉CMake要编译哪个文件、生成什么名字的可执行文件、链接哪些库。打开功能包下的CMakeLists.txt找到build区域添加add_executable(velocity_publisher src/velocity_publisher.cpp) target_link_libraries(velocity_publisher ${catkin_LIBRARIES}) add_dependencies(velocity_publisher ${catkin_EXPORTED_TARGETS})add_executable把源文件编译成可执行文件第一个参数velocity_publisher是编译产物名也就是后续rosrun publisher_tutorial velocity_publisher要用到的名字。target_link_libraries把catkin相关的库链接进来不写这条链接阶段会报一堆undefined reference。add_dependencies是为了确保自定义消息的头文件在编译本节点之前已经生成。如果只使用标准消息geometry_msgs等不写也能通过但养成习惯总没错。每次改完CMakeLists.txt建议检查一下括号是否有缺失我犯过最少三次files或者add_executable漏了右括号导致整个工作空间编译失败的问题。3.4 在C版本里多聊一句消息头的stamp该怎么处理std_msgs/Header里有seq、stamp、frame_id三个字段geometry_msgs/Twist不包含Header所以暂时不用管。但如果你以后写nav_msgs/Odometry这类自带Header的消息一定要给stamp赋值比如msg.header.stamp ros::Time::now()。原因是接收方做时间同步、坐标变换时都依赖这个时间戳不赋值就等于给下游数据打了一个未知时间标签很多算法会直接拒收。这个知识点我在第一次写里程计发布者的时候踩过坑后面避坑章节还会说到。4. Python版Publisher适合快速验证的另一种写法如果你做过原型验证、算法调试通常更喜欢Python。ROS里的rospy实现的Publisher写法更简洁省去了编译环节改完代码直接跑。这个版本我建议至少跟着写一遍不是为了偷懒而是为了对比两种语言在同一个话题通信模型里的共性。4.1 完整代码同样的速度发布逻辑十行搞定在包的scripts目录下创建velocity_publisher.py没有scripts目录就mkdir一个#!/usr/bin/env python3 import rospy from geometry_msgs.msg import Twist def main(): rospy.init_node(velocity_publisher_py, anonymousTrue) pub rospy.Publisher(/cmd_vel, Twist, queue_size100) rate rospy.Rate(10) while not rospy.is_shutdown(): vel_msg Twist() vel_msg.linear.x 0.5 vel_msg.angular.z 0.2 pub.publish(vel_msg) rospy.loginfo(Publishing: linear.x %.2f, angular.z %.2f, vel_msg.linear.x, vel_msg.angular.z) rate.sleep() if __name__ __main__: main()运行前记得给脚本加可执行权限chmod x scripts/velocity_publisher.py4.2 Python版几个值得注意的差异点anonymousTrue这个参数会在节点名字后面自动追加随机数字。如果你只开一个此节点写不写都行但如果要同时跑多个相同脚本不写就会节点名冲突写了就能自动避免冲突。我在调试多机通信、同时模拟多个机器人时这个参数帮我省了很多麻烦。queue_size100在rospy里这个queue_size的行为比C更直观——缓存最近100条尚未被订阅者取走的消息。在ROS Noetic里rospy.Publisher的queue_size已经是必填参数不填会在运行时报TypeError。rospy.loginfo相当于C里的ROS_INFO但会额外附带节点名和时间。调试阶段我习惯用loginfo而不是print因为输出里能一眼看到消息是哪个节点打的日志也更符合ROS的格式规范。4.3 为什么不建议在Python里发布高频大数据量话题Python的rospy在发布海量点云、图像这类高频大数据时性能明显不如C的roscpp。我自己做过一个简单测试同一台机器发布sensor_msgs/ImageC能达到30Hz稳定Python只能到20Hz左右而且CPU占用高出一截。所以我的经验是逻辑调试用Python、传感器高频数据用C。像/cmd_vel这种低频小数据Python完全够用这也是为什么很多机器人的导航框架里远程遥控节点用Python写、底盘驱动节点用C写。4.4 要是你打算发自定义消息类型Python这边多一步Python版如果只发标准消息类型代码里import一下就行。要是想发自定义消息比如说my_pkg/MyMsg除了要在CMakeLists和package.xml里配置依赖还得确保生成的Python模块路径在环境里——通常catkin_make之后devel/lib/python3/dist-packages下的消息模块会被自动加入PYTHONPATH。如果运行时提示找不到module先检查你是否source了devel/setup.bash。5. 编译、运行与验证怎么确认消息真的发出去了这一节非常关键。因为Publisher发布之后屏幕上看到的只有ROS_INFO打印你其实并不知道数据到底有没有进入ROS网络。正确的做法是用ROS自带命令行工具来监听话题流量拿rostopic系列命令验证。5.1 编译C节点并确认生成可执行文件回到工作空间根目录cd ~/catkin_ws catkin_make看到100%之后检查一下编译产物ls devel/lib/publisher_tutorial/应该能看到velocity_publisher这个可执行文件。如果找不到说明CMakeLists.txt里的add_executable写法和你的文件名对不上或者编译过程其实报了错。这里必须提醒一句每次改完代码都重新catkin_make一次。ROS不会自动感知源文件变化你不编译就运行跑的还是老版本。这个坑有一次浪费了我两个小时改完代码忘了编译反复验证一个不存在的bug。5.2 启动roscore、运行节点、三连验证第一步启动ROS Master名字服务器roscore正常会看到started core service [/rosout]一类输出。roscore是ROS 1里必须存在的进程你可以理解为节点注册中心。第二步另开终端运行节点source ~/catkin_ws/devel/setup.bash rosrun publisher_tutorial velocity_publisher第三步验证话题是否存在、消息内容、发布频率。新开第三个终端# 1. 查看话题列表 rostopic list # 2. 查看消息内容实时打印 rostopic echo /cmd_vel # 3. 查看发布频率 rostopic hz /cmd_velrostopic list能确认/cmd_vel话题被创建rostopic echo能实时看到linear.x0.5、angular.z0.2这样的数据流rostopic hz能显示average rate: 10.0证明10Hz发布频率真实有效。我自己的习惯是先echo看内容再hz看频率然后再topic info /cmd_vel查看消息类型和发布者订阅者信息rostopic info /cmd_vel这个命令会列出话题类型、Publisher和Subscriber的节点名、URI地址是排查为什么收不到数据的第一站。5.3 让一台小乌龟动起来最直观的Publisher验收实验光看命令行输出你可能还是没感觉。ROS自带的turtlesim小乌龟是最好的验证工具。新开终端rosrun turtlesim turtlesim_node然后运行你写的velocity_publisher节点。如果消息类型是/turtle1/cmd_vel注意小乌龟默认话题名不一样你可以改代码里的话题名或者用remap参数话题重映射rosrun publisher_tutorial velocity_publisher /cmd_vel:/turtle1/cmd_vel运行后小乌龟会原地打转或者走圆弧——这就是你亲手发布的Twist消息被另一个节点接收并消费的全过程。整个链路闭环了你写了一个Publisher发出数据另一节点Subscriber收到产生了物理效果。5.4 运行Python版验证教程里CtrlC与日志输出Python版运行方式rosrun publisher_tutorial velocity_publisher.py注意rosrun后面找的是scripts目录下可执行权限的.py文件。如果找不到文件检查chmod是否执行了或者确认scripts目录在功能包根目录下。用CtrlC退出时rospy会先打断rate.sleep()然后进入清理流程把该释放的节点资源释放掉这个过程中你可能还会看到一条shutdown的日志属于正常现象。6. 写Publisher时最容易踩的坑和我的排查思路写到这里大部分教程就结束了。但真正从抄代码能跑到自己写代码不翻车中间还隔着一堆边界条件和工程习惯。我记录几个实际踩过的坑每个坑都附带排查链路希望能帮读者省掉我当初的折腾时间。6.1 找不到功能包 / 找不到节点八成是你没source运行rosrun publisher_tutorial velocity_publisher报错[rosrun] Couldnt find executable named ...或者package [publisher_tutorial] not found大多数情况下不是包写错了而是当前终端的环境变量里没有你的工作空间。排查链路echo $ROS_PACKAGE_PATH看看输出里有没有/home/你的用户名/catkin_ws/src。如果没有执行source ~/catkin_ws/devel/setup.bash再跑rosrun。如果确认source了还不行检查package.xml里的包名和你rosrun敲的包名是否完全一致注意大小写和下划线。6.2 节点能启动但话题没出现初始化与发布者对象生命周期问题有一次我在一个函数里局部创建ros::Publisher函数结束Publisher就析构了话题时有时无。Publisher对象必须在循环之外、长期存活的作用域里。如果在循环内部反复advertise和析构会不断注册和注销话题rostopic list里的话题会闪一闪但没实际数据流。排查方法先rostopic list看话题在不在如果不在回到代码检查Publisher对象的生命周期。6.3 发布频率对不上rate.sleep()和publish()阻塞的关系C的publish()只在缓冲区满时短暂阻塞Python的publish()本质也是入队。如果你发现rostopic hz输出的实际频率远低于设定值比如设了100Hz实际只有5Hz大概率是消息构造、拷贝或回调占用了大量时间loop_rate.sleep()弥补不了这些开销。这类问题不要只在代码里找原因可以先用rostopic hz量化偏差再用top看CPU占用最后才考虑是不是该精简消息、降低频率或者把大消息发布换成C实现。6.4 多个节点抢同一个话题底盘节点到底听谁的热词里有个问题很真实ros多个节点发布移动指令话题时底盘节点如何取舍。我遇到过遥控节点发/cmd_vel导航节点也发/cmd_vel两个同时跑机器人走走停停、抽搐一样。原因是两个Publisher往同一个话题发数据底盘驱动节点哪条都收每秒钟可能收到好几条指令相当于两个管理员同时发号施令。ROS本身不做仲裁它只负责把数据转发给所有订阅者。我建议的取舍策略有三种不同场景不同节点遥控时只跑遥控节点导航时只跑导航节点人工确保互斥。用/cmd_vel和/cmd_vel_teleop分开话题底盘节点通过参数配置选择监听哪个话题。上层做仲裁底盘驱动节点里写一个优先级逻辑比如检测到遥控数据就屏蔽导航数据。具体的取舍和机器人安全性直接相关这是我强烈建议在写Publisher之前就想清楚的设计问题不要等物理世界出了问题再来排查。6.5 忘记消息头的时间戳下游数据有时间悖论前面提到发带Header的消息Odometry、LaserScan、Image等时一定要给msg.header.stamp赋值。如果你不发时间戳有的板子驱动会用接收时刻填充有的干脆是空的这会导致下游的tf坐标变换、message_filters时间同步直接报Header has no timestamp或丢弃数据。一个小技巧在C里用ros::Time::now()赋值在Python里用rospy.Time.now()赋值。代价极小但能省掉一堆看似莫名其妙的后续bug。6.6 单话题多数据类型不要指望一个Publisher发两种消息Publisher对象在创建时绑定了消息类型一个Publisher只能发一种类型的数据。想发不同类型数据就创建多个Publisher分别advertise到不同话题。我在早期试图用一个循环把/cmd_vel和/odom混在一个话题里发然后订阅端根本无法解析教训深刻。6.7 关于多机通信的简单提醒如果你要把话题跨电脑传输比如一台电脑发/cmd_vel另一台电脑的底盘驱动接收除了两边都要有相同的消息定义之外核心是设置正确的ROS_MASTER_URI指向运行roscore那台机器并确保两台机器在同一个局域网内。这个话题值得单独开一篇这里只提一句Publisher本身的代码写法不需要改动。7. 让Publisher更皮实的一点点进阶习惯写到这里Publisher的基本功就全了。不过如果你准备长期写ROS节点我还有几个小习惯想分享都是实操中沉淀出来的不算复杂但很管用。7.1 发布参数化不要硬编码话题名和频率我早期写Publisher很直接话题名和频率写在代码里。后来发现每次换机器人、换场景就得改代码重编译非常麻烦。建议用ros::param或启动文件launch file参数重映射来控制这些值。比如在launch文件里launch node namevelocity_publisher pkgpublisher_tutorial typevelocity_publisher outputscreen param nametopic_name value/cmd_vel / param namepublish_rate value20.0 / /node /launch代码里用n.param(topic_name, std::string(/cmd_vel))和n.param(publish_rate, 10.0)去读。这样你可以不改代码就调整频率和话题名调试效率提升特别明显。7.2 发布前先做数据合法性检查发布/cmd_vel这种直接控制电机的指令时一个异常的速度值可能直接让机器人窜出去或者原地打转。我建议在publish()之前加一个简单的范围判断比如线速度限幅、角速度限幅。看似多写几行但关键时刻能兜底。尤其当你从订阅节点收到数据再转发时数据来源不可控更要做一层保护。7.3 动态重配置和心跳检测如果你在做一个长期运行的机器人系统可以考虑在话题中加一个心跳字段比如连续发布时间戳或者用ros::Time::now()发。底盘驱动节点可以通过如果超过500毫秒没收到新指令就自动停车来实现安全超时。大部分商用底盘驱动都自带这个逻辑但你自己写的测试节点往往没有。如果底盘没有安全超时机制而你的Publisher在发完指令后崩溃了机器人就会保持最后一条速度指令一直跑下去——这是非常危险的事故场景。我个人在测试时遇到过一次节点被CtrlC杀掉但运动指令已经发出小车继续保持最后一个速度前进。那一次我意识到发布者代码虽然简单但工程意识和安全兜底不能省。7.4 命名规范和话题名规划话题名不是随便起的。ROS社区惯例是小写字母下划线有层级关系用/分隔例如/robot_base/cmd_vel。建议你在项目初期就把命名规划好不然等节点多了/cmd_vel、/cmd_vel1、/cmdvel并存光排查名字匹配就能让人抓狂。写这篇笔记的时候我又把当初第一个Publisher的代码翻出来看了一遍代码没怎么变但心态变了很多。以前觉得Publisher就是模板代码能跑就行现在再看它是一个完整通信链路的起点是你和机器人之间最基础的信息交互方式。理解话题、消息和节点的关系比抄代码重要得多验证数据是否真正到达了订阅端比让终端打印几行日志重要得多安全兜底和话题命名规划比追求代码写得花哨重要得多。希望这篇能帮你顺顺利利把第一个Publisher跑通然后在这个基础上继续去折腾Subscriber、Service、Action——ROS这条深水区值得慢慢趟。