写第一个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 2(Humble/Foxy),命令和代码风格会差很多。本文以ROS 1为主,但核心思想在ROS 2里同样适用,只是advertise变成了create_publisher,Master变成了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>同时覆盖两种情况。
<depend>roscpp</depend> <depend>rospy</depend> <depend>std_msgs</depend> <depend>geometry_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.advertise<geometry_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.advertise<geometry_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()的判断,是为了能优雅地退出循环。Ctrl+C时ros::ok()会返回false,程序能正常跳出循环、走析构清理资源。如果你写成while (true),Ctrl+C可能直接崩溃或者留下孤儿节点。
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", anonymous=True) pub = rospy.Publisher("/cmd_vel", Twist, queue_size=100) 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版几个值得注意的差异点
anonymous=True:这个参数会在节点名字后面自动追加随机数字。如果你只开一个此节点,写不写都行;但如果要同时跑多个相同脚本,不写就会节点名冲突,写了就能自动避免冲突。我在调试多机通信、同时模拟多个机器人时,这个参数帮我省了很多麻烦。
queue_size=100:在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/Image,C++能达到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.x=0.5、angular.z=0.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版,验证教程里"Ctrl+C"与日志输出
Python版运行方式:
rosrun publisher_tutorial velocity_publisher.py注意rosrun后面找的是scripts目录下可执行权限的.py文件。如果找不到文件,检查chmod是否执行了,或者确认scripts目录在功能包根目录下。用Ctrl+C退出时,rospy会先打断rate.sleep(),然后进入清理流程,把该释放的节点资源释放掉,这个过程中你可能还会看到一条shutdown的日志,属于正常现象。
6. 写Publisher时最容易踩的坑和我的排查思路
写到这里,大部分教程就结束了。但真正从"抄代码能跑"到"自己写代码不翻车",中间还隔着一堆边界条件和工程习惯。我记录几个实际踩过的坑,每个坑都附带排查链路,希望能帮读者省掉我当初的折腾时间。
6.1 找不到功能包 / 找不到节点:八成是你没source
运行rosrun publisher_tutorial velocity_publisher,报错[rosrun] Couldn't 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 name="velocity_publisher" pkg="publisher_tutorial" type="velocity_publisher" output="screen"> <param name="topic_name" value="/cmd_vel" /> <param name="publish_rate" value="20.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在发完指令后崩溃了,机器人就会保持最后一条速度指令一直跑下去——这是非常危险的事故场景。
我个人在测试时遇到过一次:节点被Ctrl+C杀掉,但运动指令已经发出,小车继续保持最后一个速度前进。那一次我意识到,发布者代码虽然简单,但工程意识和安全兜底不能省。
7.4 命名规范和话题名规划
话题名不是随便起的。ROS社区惯例是:小写字母+下划线,有层级关系用/分隔,例如/robot_base/cmd_vel。建议你在项目初期就把命名规划好,不然等节点多了,/cmd_vel、/cmd_vel1、/cmdvel并存,光排查名字匹配就能让人抓狂。
写这篇笔记的时候,我又把当初第一个Publisher的代码翻出来看了一遍,代码没怎么变,但心态变了很多。以前觉得Publisher就是"模板代码",能跑就行;现在再看,它是一个完整通信链路的起点,是你和机器人之间最基础的信息交互方式。理解话题、消息和节点的关系比抄代码重要得多;验证数据是否真正到达了订阅端,比让终端打印几行日志重要得多;安全兜底和话题命名规划,比追求代码写得花哨重要得多。希望这篇能帮你顺顺利利把第一个Publisher跑通,然后在这个基础上,继续去折腾Subscriber、Service、Action——ROS这条深水区,值得慢慢趟。