ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ROS开发入门:从工程搭建到话题通信实战

ROS开发入门:从工程搭建到话题通信实战 做ROS开发的人十有八九都经历过这样的场景教程翻了一大堆环境好不容易装上打开终端却不知道第一行命令该敲什么。别笑我当年也是这样。这篇东西就围绕工程搭建、节点编写、launch 文件和话题通信这条主线把 ROS 开发里最容易卡住新手的几个点挨个讲透。你不需要有任何基础只要 ROS 环境能跑起来跟着一步步操作把 talker 和 listener 这两个节点“撮合”到一起基本就算是迈过 ROS 的第一道门槛了。我默认你用的是 Ubuntu 20.04 ROS Noetic这是目前 ROS 1 里资料最全、社区最活跃的组合。文章里所有命令、代码我都实测过你可以直接抄作业。如果环境还没装好我建议先解决环境问题再来读这篇否则光看不练等于白看。1. 工程搭建先把“地盘”收拾干净1.1 环境选择和安装的讲究ROS 有版本对应关系的硬约束Noetic 只支持 Ubuntu 20.04哪怕你换了 20.10 都可能有一堆依赖问题。很多新手第一步就倒在版本不匹配上所以在装系统之前请先确认你的 Ubuntu 版本和 ROS 版本是一一对应的。国内很多人喜欢用鱼香ROS的一键安装脚本实测对新手确实省事它会自动换镜像源还能省掉不少网络上的折腾。不过我的建议是用一键安装没问题但装完之后最好自己手动source /opt/ros/noetic/setup.bash一次知道自己的 ROS 环境到底装在哪个目录下。否则后面排查问题时真的会两眼一抹黑。我在带新人的时候还发现一个现象不少人在虚拟机里装 ROS这本身没问题但虚拟机默认的内存和 CPU 给的太少编译一个 catkin 工作空间都能卡到怀疑人生。建议给虚拟机至少 4GB 内存、双核 CPU硬盘 30GB 以上。如果机器配置实在不行也别硬撑找台旧电脑直接装双系统体验会好很多。1.2 工作空间的创建理解 catkin 的逻辑装完 ROS 之后第一件事是创建你的工作空间。在 ROS 里工作空间通常叫catkin_ws它本质上就是一个文件夹里面要有固定的子目录结构src放源代码build放编译缓存devel放编译出来的可执行文件和环境变量。你平时只需要关心src目录后面两个都是自动生成的。mkdir -p ~/catkin_ws/src cd ~/catkin_ws catkin_make第一次执行catkin_make会创建build和devel目录代表工作空间初始化成功。紧接着做一件很多教程都会忽略但极其重要的事echo source ~/catkin_ws/devel/setup.bash ~/.bashrc source ~/.bashrc如果不添加这一行你新开的终端默认是找不到自己工作空间里编译出来的功能包的。我见过太多人后面编译成功但跑不起来60% 都是因为忘了这一步。接下来在src下创建功能包。功能包这个名字很容易让人困惑其实它就是一个普通的文件夹里面放着你的代码、配置文件和一个说明文件。创建命令如下cd ~/catkin_ws/src catkin_create_pkg beginner_tutorials roscpp rospy std_msgs命令里最后的roscpp rospy std_msgs是这个包要依赖的库。roscpp是 C 的 ROS 库rospy是 Python 的 ROS 库std_msgs是标准消息定义几乎所有入门包都会带上这三个。创建完成后你会在beginner_tutorials目录下看到两个灵魂文件CMakeLists.txt和package.xml。前者告诉编译器怎么编译你的代码后者描述这个包的依赖关系和信息。这里必须多说一句很多人觉得package.xml就是个摆设实际上它的作用非常大。当你用rosdep安装系统依赖时它读的就是这个文件当你把一个包发给其他人时对方靠它知道需要预装哪些依赖。新手阶段不要求写得多完美但里面build_depend和exec_depend这两项别删错。有些教程让初学者精简掉这两个标签结果后来编译时一堆找不到头文件的报错就是埋下的坑。1.3 为什么是 catkin 而不是普通 make猫和肉丝的比喻我听很多人讲过catkin_make其实就是对 CMake 的封装在传统 CMake 的基础上增加了 ROS 包之间的依赖管理和环境钩子。它的核心优势只有一个你在src下放各种功能包它自动帮你搞定包与包之间的编译顺序。比如 A 包依赖 B 包里生成的消息catkin_make会先编译 B 再编译 A这在手动 CMake 里非常容易搞错。换个角度理解catkin_make有点像一个自动化的工地包工头你知道要盖哪几栋楼功能包他帮你排施工顺序、处理材料头文件和库文件的调度。这样你就能把精力放在写代码上而不是纠结编译依赖。了解这一点之后再看到catkin_make那满屏的日志就不会觉得它神秘了。2. 节点编写让代码跑起来才是硬道理2.1 节点、话题、消息先建立画面感很多人一上来就背概念节点是进程话题是总线消息是数据结构。背完了还是不知道怎么用。我更喜欢用出版社和读者的关系来类比。一个 ROS 节点就是一个独立运行的程序它可以是一个发布者Publisher也可以是一个订阅者Subscriber或者两者都是。发布者把消息“贴”到一个叫话题Topic的公告栏上订阅者则去公告栏上读自己感兴趣的内容。话题本身不存储数据它只是一个名字比如/chatter、/cmd_vel就像公告栏的栏目标题。消息就是贴在公告栏上的具体内容比如字符串、坐标、速度指令。关键点是发布者和订阅者都不知道对方是谁它们只认话题名和消息类型。这个设计的巧妙之处在于解耦。发布者只管发订阅者只管收谁挂了都不影响另一方而且同一话题可以有多个发布者、多个订阅者。这在真实机器人系统里太重要了传感器驱动节点和导航节点之间互不认识只要话题对得上就能协同工作。2.2 Python 节点实例写一个能跑的最小发布者先别碰复杂逻辑我带你写一个最精简的发布者节点。在beginner_tutorials包目录下新建一个scripts/talker.py内容如下#!/usr/bin/env python3 import rospy from std_msgs.msg import String def talker(): rospy.init_node(talker) pub rospy.Publisher(chatter, String, queue_size10) rate rospy.Rate(1) # 每秒发送1次 while not rospy.is_shutdown(): msg String() msg.data hello world %s % rospy.get_time() pub.publish(msg) rate.sleep() if __name__ __main__: try: talker() except rospy.ROSInterruptException: pass逐行解释一下。rospy.init_node(talker)是每个节点的灵魂它在 ROS 网络中注册这个节点的名字。名字如果重复后启动的那个节点会被强制改名ROS 会礼貌地提醒但你最好别依赖这个特性。rospy.Publisher(chatter, String, queue_size10)里有三个参数话题名、消息类型、队列长度。队列长度是个容易忽略的细节表示在网络繁忙时最多缓存多少条消息。设得太小短时间突发消息会丢设得太大内存开销高延时也可能变大。初学阶段设 10 完全够了。rospy.Rate(1)控制循环频率rate.sleep()让循环按照设定频率执行。这个写法是 ROS 节点最典型的主循环模式循环里不断发布消息每次循环结束睡一下控制节奏。然后是订阅者新建scripts/listener.py#!/usr/bin/env python3 import rospy from std_msgs.msg import String def callback(msg): rospy.loginfo(I heard: %s, msg.data) def listener(): rospy.init_node(listener) rospy.Subscriber(chatter, String, callback) rospy.spin() if __name__ __main__: listener()订阅者的重点是回调函数。ROS 是事件驱动的当/chatter上有新消息到达时callback会被自动调用消息通过参数传进来。rospy.spin()看着简单实际上它启动了一个后台循环不断处理收到的消息并调度回调函数。新手最容易犯的错是忘记写spin()结果消息倒是收到了回调却一次都没触发还以为是网络问题。别忘了一步给脚本加上可执行权限否则rosrun会提示找不到可执行文件。chmod x scripts/talker.py scripts/listener.py2.3 C 节点与 CMakeLists 配置用 Python 跑通之后我强烈建议再写一次 C 版本因为很多机器人底层库只提供 C 接口。在beginner_tutorials/src/talker.cpp里写#include ros/ros.h #include std_msgs/String.h #include sstream int main(int argc, char **argv) { ros::init(argc, argv, talker); ros::NodeHandle n; ros::Publisher chatter_pub n.advertisestd_msgs::String(chatter, 1000); ros::Rate loop_rate(10); int count 0; while (ros::ok()) { std_msgs::String msg; std::stringstream ss; ss hello world count; msg.data ss.str(); ROS_INFO(%s, msg.data.c_str()); chatter_pub.publish(msg); ros::spinOnce(); loop_rate.sleep(); count; } return 0; }C 和 Python 的节点逻辑几乎一一对应。唯一的明显区别是C 里发布消息后要调用ros::spinOnce()这是处理回调函数的入口。在 C 循环里如果你订阅话题但循环里不调spinOnce回调函数永远不会执行。这是从 Python 转到 C 最容易踩的第一个坑。编译配置是个重灾区。打开CMakeLists.txt在末尾追加这些行add_executable(talker src/talker.cpp) target_link_libraries(talker ${catkin_LIBRARIES}) add_executable(listener src/listener.cpp) target_link_libraries(listener ${catkin_LIBRARIES})然后回到工作空间根目录编译cd ~/catkin_ws catkin_make如果编译不通过90% 的情况是find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs)那一行没列出你需要的组件或者target_link_libraries写错。我建议先检查 CMakeLists 里头部的那一行把用到的组件都加上再回头看 add_executable 里文件路径是不是对上了。路径错误很常见比如建了src/目录但代码放在别处编译就会找不到源文件。2.4 运行与验证怎么判断两个节点真的通信了编译成功后三个终端分工干活。第一个终端启动 master——在 ROS 1 里叫roscore它是整个系统的基础服务负责节点之间的相互发现和话题注册roscore第二个终端启动发布者第三个终端启动订阅者source ~/catkin_ws/devel/setup.bash rosrun beginner_tutorials talker.pysource ~/catkin_ws/devel/setup.bash rosrun beginner_tutorials listener.py如果一切正常listener 的终端会持续打印I heard: hello world ...。这时候你才算真正跑通了 ROS 的“Hello World”。为了验证真的“听到”了我再教你一个核心诊断命令rostopic echo /chatter这个命令会直接监听/chatter话题把发布者发的所有消息内容实时打印出来。它不依赖你的 listener 节点是独立的第三方订阅者。如果rostopic echo能看到消息说明话题层面没有问题问题只能出在你自己的 listener 代码里。这种“从外到内”的排查思路比盯着自己的代码发呆高效多了。3. launch 文件一次启动一窝进程3.1 为什么需要 launch 文件跑通 talker 和 listener 之后你马上会意识到一个问题每启动一个节点就要开一个终端以后节点一多几十个终端扑面而来光找窗口就把人逼疯。launch文件就是用来解决这个问题的。它就像你家的智能遥控器按一个按钮电视、音响、机顶盒全给你打开而不是让你挨个去按电源键。launch文件本质上是一个 XML 格式的配置文件roslaunch命令根据它的内容批量启动里面的所有节点还能顺便设置参数、修改话题名、管理节点命名空间。它不是 ROS 建筑的必需品但一旦用过你就回不去了。3.2 核心语法与标签五分钟上手我直接给你一个可以立刻用的例子在beginner_tutorials目录下新建launch/talker_listener.launchlaunch node nametalker pkgbeginner_tutorials typetalker.py outputscreen/ node namelistener pkgbeginner_tutorials typelistener.py outputscreen/ /launch然后运行roslaunch beginner_tutorials talker_listener.launch两个节点瞬间同时启动。细节来了type字段写的是可执行文件的名字对 Python 脚本来说就是文件名对 C 程序来说是你在 CMakeLists 里add_executable指定的目标名两者经常不一致。很多人把type写成源文件路径或者写错大小写结果roslaunch报错说找不到节点。outputscreen这个属性特别重要。默认情况下节点的输出会写到日志文件里你根本看不到printf或print的内容。加上它输出才会显示到当前终端。排查节点启动后没反应的问题时我第一步就是看它的输出里有没有报错。再介绍两个常用标签param namespeed value0.5/ remap from/cmd_vel to/turtle1/cmd_vel/param用来向 ROS 参数服务器写入参数任何节点都可以通过rospy.get_param或ros::param::get读出来相当于给所有节点发一份共享配置表。remap则更神奇它能把节点代码里写死的话题名偷偷改成另一个名字不用改代码就能让原本话题对不上的节点“连上线”。3.3 launch 文件调试中的常见坑我在实际调试 launch 文件时踩过几个坑分享给你。第一个是 XML 格式极其容易写错尤其标签必须一一配对。很多人漏了闭合标签或引号不对称roslaunch 直接报 XML parsing error。建议写完 launch 文件先在意 IDE 里打开看有没有语法高亮报错。第二个坑是roslaunch会在节点启动前帮你自动启动roscore所以你在roslaunch之前不需要手动开roscore。如果你已经开了roscoreroslaunch 会复用已有的不会起新的别以为启动失败。第三个坑是路径问题。launch 文件里启动的节点如果依赖相对路径读取配置文件或地图文件最好用$(find pkg_name)来定位包路径而不是写死绝对路径。比如param namemap_file value$(find beginner_tutorials)/maps/map.yaml/这样无论你的工作空间在哪台机器上只要包还在就能找到文件。我在迁移项目时因为写死绝对路径吃过不少亏换台机器就要改一遍后来统一改用$(find ...)一劳永逸。4. 话题通信实战用真实场景串起所有知识4.1 用 turtlesim 做一场看得见的通信演示理论讲太多容易困我们来点有画面感的。ROS 自带一个海龟模拟器turtlesim它本身就是一个 ROS 节点订阅/turtle1/cmd_vel话题来移动画面上的小乌龟。你完全可以用自己写的发布者节点来控制它这就构成了一个完整闭环。先启动模拟器和键盘控制节点rosrun turtlesim turtlesim_node rosrun turtlesim turtle_teleop_key这时你会发现键盘控制的终端里按方向键小乌龟就会动。它的原理是turtle_teleop_key节点监听你的键盘输入发布一个geometry_msgs/Twist类型的消息到/turtle1/cmd_vel话题turtlesim_node订阅这个话题并更新画面。这就是典型的话题通信两个节点彼此不关心对方是谁只认话题和消息类型。现在关掉键盘控制节点换你自己的脚本来发速度指令。新建scripts/vel_publisher.py#!/usr/bin/env python3 import rospy from geometry_msgs.msg import Twist def send_velocity(): rospy.init_node(vel_publisher) pub rospy.Publisher(/turtle1/cmd_vel, Twist, queue_size10) rate rospy.Rate(10) vel Twist() vel.linear.x 1.0 vel.angular.z 0.5 while not rospy.is_shutdown(): pub.publish(vel) rate.sleep() if __name__ __main__: try: send_velocity() except rospy.ROSInterruptException: pass把脚本放在beginner_tutorials/scripts/下添加可执行权限后再想办法运行。注意如果你的beginner_tutorials包没有在CMakeLists.txt里声明依赖geometry_msgs编译时会报找不到头文件/模块。需要在创建包时把geometry_msgs加进去或者打开 CMakeLists 和 package.xml 手动补上。运行之后小乌龟会自己画出一个圆弧。这时候你打开新终端执行rostopic info /turtle1/cmd_vel rostopic hz /turtle1/cmd_velrostopic hz会显示消息发布频率如果显示 10Hz 左右说明你的发布节奏是稳的。这个命令在排查“消息到底有没有发出来”时极其好用比 echo 更适合检测“有发送但频率不稳定”的问题。4.2 自定义消息类型把复杂数据打包上传std_msgs和geometry_msgs能覆盖很多场景但实际项目中你需要定义自己的数据结构。比如一个设备状态消息包含传感器温度、电量、运行时间等多个字段。msg文件就是干这个的。在beginner_tutorials包下新建msg/VelocityCommand.msg内容如下float32 linear float32 angular float32 duration然后修改CMakeLists.txt找到add_message_files部分改成add_message_files( FILES VelocityCommand.msg )还需要找到generate_messages部分确保有generate_messages( DEPENDENCIES std_msgs )同时package.xml里必须加上这两行build_dependmessage_generation/build_depend exec_dependmessage_runtime/exec_depend重新编译一次再检查生成的 Python/C 接口是否存在。然后在 Python 里就能这样用from beginner_tutorials.msg import VelocityCommand cmd VelocityCommand() cmd.linear 1.0 cmd.angular 0.3 cmd.duration 2.0自定义消息这块C 的使用方式略有不同需要#include beginner_tutorials/VelocityCommand.h然后在 CMakeLists 里注册消息依赖。新手写自定义消息最容易在package.xml和CMakeLists.txt上出问题。我的排查经验是先确认两端都加了message_generation/message_runtime再确认generate_messages里列出了它依赖的消息包最后确认用到这个消息的节点链接了对应库。三步做完基本都能过。4.3 多机分布式节点把话题通信延伸到多台电脑ROS 的话题通信天然支持跨机器。比如你的机器人是一台装有传感器的迷你主机而你在自己电脑上运行可视化工具和导航算法两台机器就可以通过局域网共享同一个 ROS 网络。配置方法很简单在主节点机器通常跑 roscore 的那台上设置export ROS_MASTER_URIhttp://主机IP:11311 export ROS_HOSTNAME主机IP在副节点机器上设置export ROS_MASTER_URIhttp://主机IP:11311 export ROS_HOSTNAME副机IP要注意的是ROS_MASTER_URI告诉所有节点去哪个地址找 master但节点之间的数据传输是点对点的双方必须能互相访问 IP这就涉及防火墙和网段设置。我自己调试多机时最常用的验证方法是在两台机器上分别运行rostopic list如果看到的话题列表一致说明整个 ROS 网络已经通了。如果只有 master 机器能看到全部话题副机看到的列表不全一般是ROS_HOSTNAME没设置对导致副机无法被其他节点发现。这个扩展内容暂时不需要深入你只需要知道你在单机上学会的节点、话题、launch 的概念在多机上完全通用区别只是多配置两个环境变量。将来做实体机器人小车时这套知识会直接派上用场。5. 常见问题与排查技巧实录5.1 环境启动失败从 rosnode 到 roscore新手最崩溃的时刻往往是输入roscore后提示命令找不到。这不是 ROS 没装好而是当前终端没有加载 ROS 的环境变量。解决办法是把加载命令写进~/.bashrcecho source /opt/ros/noetic/setup.bash ~/.bashrc source ~/.bashrc还有一种情况是roscore起不来报错信息里出现Unable to contact my own server之类的内容。这通常是因为上一轮roscore没有正常退出ROS master 默认端口11311被占用了。检查方法ps -aux | grep roscore找到残留进程后 kill 掉再重启。我建议养成习惯每次跑完实验用CtrlC正常终止roslaunch不要直接关终端。直接关终端容易留下僵尸进程端口被占用的概率会大增。5.2 编译失败三分写代码七分调 CMake我记得刚开始带一个学习者做小车项目他卡在编译错误上快三天最后发现只是add_executable里的空格多了个字符。CMake 的语法说难不难但报错信息往往绕着弯子。最常见的三类编译失败原因我整理了一个速查表报错类型常见原因排查方向Could not find the required component功能包依赖未声明或系统缺少库检查 CMakeLists 的find_package和 package.xmlCannot find source file源文件路径写错确认 add_executable 里的路径和实际文件位置一致Undefined reference to ...链接库缺失检查target_link_libraries是否列出了catkin_LIBRARIES和消息库编译通过后还有一个必须养成的习惯每次修改代码后都要回到工作空间根目录重新catkin_make并且在新终端里重新source ~/catkin_ws/devel/setup.bash。很多人改了代码跑起来还是旧逻辑就是因为忘了重新编译或 source。5.3 话题通信异常数据没通的诊断思路如果你已经运行了 talker 和 listenerlistener 却没有任何输出按以下顺序排查。第一步确认三个终端里的话题名是否一致。大小写、下划线、斜杠前缀任何差异都会导致订阅不到消息。尤其是 Python 端的 Publisher 写了chatterC 端却订阅/chatter这俩在 ROS 1 里会被视为不同话题。第二步用rostopic list查看当前网络里到底有哪些话题。如果chatter根本不在列表里说明发布者还没成功注册检查发布者节点是否正常运行。如果列表里有chatter但 echo 没数据可能是发布频率本来就低比如Rate(0.01)那就是 100 秒发一次等一会儿就有了。第三步看回调有没有被触发。Python 里检查是否调用了rospy.spin()C 里检查循环中是否调用了ros::spinOnce()。这是最容易被忽视的Python 和 C 的事件处理机制不同但从现象看都是“好像没收到消息”。5.4 权限与异常报错那些不为人注意的小问题最后说几个容易被忽略但很影响体验的问题。Python 脚本加了#!/usr/bin/env python3但没有执行权限rosrun会提示找不到节点而不是权限不足。解决方法chmod x scripts/*.pylaunch 文件里节点输出不可见检查有没有加outputscreen。没有的话节点的标准输出会进入日志文件你看起来就像节点什么都没干实际上它在正常工作。另外roslaunch已经自动启动了 roscore你不需要在它之前手动开 roscore。如果手动开了再 roslaunch 时有时会因网络配置问题报ROS_MASTER_URI不匹配这时干脆全部关掉只用 roslaunch 一个命令重新启动就好。高频出现的还有 ROS_MASTER_URI 被修改后没恢复。有些人之前做过多机调试改过环境变量再回到单机却发现节点之间不通了。解决办法是在~/.bashrc里检查有没有export ROS_MASTER_URI这行如果有并且指向别的 IP删掉或者改回http://localhost:11311。提示涉及 USB 串口设备时比如接激光雷达或摄像头串口权限问题也很常见。把用户加入dialout组通常能解决sudo usermod -aG dialout $USER注销重登生效。这类问题不属于 ROS 本身但会频繁出现在 ROS 项目里提前准备好能省很多事。我自己带过不少新手体会最深的一点是ROS 入门最忌讳只看不练。教程读十遍不如把 talker 和 listener 亲手跑通一遍。你只要让两个节点通过话题说上话后面再学服务、动作、参数服务器就都是在同一套逻辑上的扩展了。如果这篇文章能帮你把第一个节点跑起来下一步可以试着用rostopic pub直接在命令行里发布速度指令或者用 launch 文件一次性启动乌龟模拟器和键盘控制感受一下“一键开车”的乐趣。踩坑的时候不要慌把报错信息贴到搜索引擎里你会发现几乎每一个坑都有成千上万的人陪你一起踩过。
返回列表