资讯动态

ROS机器人开发实践源码解析:从catkin工作空间到话题服务与TF坐标变换

发布时间:2026/10/20 10:49:58 来源:尧图企业网站定制
简介这份资源是《ROS机器人开发实践》一书的配套源码包面向正在学习机器人操作系统、希望从理论走向动手实践的开发者与高校学生。内容按学习路径划分为ROS基础功能、ROS进阶功能、机器人感知、机器学习以及ROS2等模块并包含mrobot与marm相关功能包覆盖节点管理、话题与服务通信、参数服务器、导航与SLAM、多机器人协同等核心知识点适合作为课程实验或自学练手的代码参考。压缩包共839个文件约146.12MB以222个Python脚本、87个launch启动文件、27个C源文件为主辅以xacro、urdf、rviz配置、yaml参数、world仿真场景及proto、msg、srv等接口定义另有aiml对话语料与模型权重文件结构完整、层次分明。目前已有1137人学习下载。读者可借助这些可运行示例快速搭建ROS1与ROS2工作空间对照书中案例调试功能包理解消息传递与仿真流程并积累排错经验为机器人系统设计与开发打下扎实基础。1. 从一份 ROS 源码包说起机器人开发到底在跑什么很多人第一次接触机器人开发是从装完 ROS 环境、敲下roscore那一刻开始的。终端里滚出一串节点注册日志RViz 里跳出一个灰色网格然后呢然后就没有然后了。教程让你跑turtlesim你跑通了但关掉窗口之后脑子里留下的只有一个会转圈的小乌龟和一堆不知道干什么用的rosrun、roslaunch命令。这份《ROS机器人开发实践》配套源码解决的恰恰是这个断层——它把「能跑通」和「知道为什么能跑通」之间的那段路用可编译、可修改的工程代码铺了出来。源码覆盖 ROS 基础通信、TF 坐标变换、传感器数据接入、导航栈配置、机械臂运动规划等模块适合已经装好 ROS、但面对空白工作空间不知道从哪下手的人。下面我按自己拆包的习惯从目录结构一路讲到二次开发时怎么改参数、怎么加自己的节点。2. 工程结构拆解从 catkin 工作空间到功能包依赖拿到一份 ROS 源码第一件事不是急着catkin_make而是先看清楚它怎么组织工作空间。ROS 的构建系统 catkin 有一套约定俗成的目录规范不按这个规范来编译报错能让你怀疑人生。这份源码的顶层结构是典型的 catkin workspace 布局我把它拆成几个关键部分来看。2.1 工作空间四层目录与功能包定位一个标准的 catkin 工作空间长这样catkin_ws/ ├── src/ # 源码空间所有功能包放这里 │ ├── CMakeLists.txt # 顶层编译入口由 catkin_init_workspace 生成 │ ├── package_a/ # 功能包 A │ │ ├── CMakeLists.txt │ │ ├── package.xml │ │ ├── include/ │ │ ├── src/ │ │ ├── launch/ │ │ ├── config/ │ │ └── scripts/ │ └── package_b/ ├── build/ # 编译中间文件不用管 ├── devel/ # 开发空间setup.bash 在这里 └── install/ # 安装空间一般用不到src是唯一需要你手动维护的目录。build和devel是catkin_make自动生成的删掉重新编译就行不用心疼。devel/lib下面放编译出来的可执行文件devel/include放自动生成的消息头文件。每次打开新终端要source devel/setup.bash否则rosrun找不到你的包——这个坑我踩过不止一次终端里敲rosrun提示[rospack] Error: package xxx not found九成是忘了 source。功能包内部package.xml声明依赖CMakeLists.txt定义编译规则。这两个文件是 ROS 编译系统的核心改任何编译选项都得从这里入手。launch目录放启动文件config放参数配置YAML 格式居多scripts放 Python 脚本src放 C 源文件。这套结构不是强制的但社区默认这么干你按这个来别人接手你的代码不用问路。2.2 package.xml 与 CMakeLists.txt 的依赖声明package.xml里最容易出问题的是依赖类型。常见的有build_depend、exec_depend、build_export_dependROS 2 里还多了buildtool_depend的细分。这份源码用的是 ROS 1 的格式build_depend和run_depend分开写。如果你把运行时才需要的包写成了build_depend编译能过跑起来就报libxxx.so not found。!-- package.xml 片段依赖声明 -- package format2 namerobot_basics/name version0.1.0/version descriptionROS 基础功能示例包/description maintainer emaildevexample.comdev/maintainer licenseBSD/license !-- 编译时依赖头文件、库 -- build_dependroscpp/build_depend build_dependstd_msgs/build_depend build_dependsensor_msgs/build_depend build_dependtf/build_depend !-- 运行时依赖节点启动后加载的库和包 -- exec_dependroscpp/exec_depend exec_dependstd_msgs/exec_depend exec_dependsensor_msgs/exec_depend exec_dependtf/exec_depend exec_dependrobot_state_publisher/exec_depend exec_dependjoint_state_publisher/exec_depend /packagebuild_depend告诉编译器去哪找头文件exec_depend告诉运行时去哪找动态库和可执行文件。两者都写全别偷懒。roscpp和std_msgs几乎每个包都要sensor_msgs处理激光雷达和摄像头数据时用tf做坐标变换时用。robot_state_publisher和joint_state_publisher是可视化机械臂模型时的标配少了它们 RViz 里只能看到一堆散件。对应的CMakeLists.txt里find_package要把这些依赖都列上catkin_package里声明导出的库和消息add_executable加target_link_libraries生成可执行文件。顺序不能乱find_package必须在catkin_package之前否则编译报Project xxx specifies xxx as a dependency but it was not found。# CMakeLists.txt 片段编译配置 cmake_minimum_required(VERSION 3.0.2) project(robot_basics) find_package(catkin REQUIRED COMPONENTS roscpp std_msgs sensor_msgs tf ) catkin_package( INCLUDE_DIRS include LIBRARIES robot_basics CATKIN_DEPENDS roscpp std_msgs sensor_msgs tf ) include_directories( include ${catkin_INCLUDE_DIRS} ) # 生成可执行文件节点名 源文件 add_executable(talker_node src/talker.cpp) target_link_libraries(talker_node ${catkin_LIBRARIES}) add_executable(listener_node src/listener.cpp) target_link_libraries(listener_node ${catkin_LIBRARIES})add_executable的第一个参数是生成的可执行文件名rosrun时用的就是这个名字。target_link_libraries把 catkin 找到的库链上去漏了这行链接阶段报undefined reference to xxx。catkin_package里的CATKIN_DEPENDS是给依赖你这个包的其他包看的自己编译时用不到但别省否则别人find_package你的包会失败。2.3 launch 文件组织与参数服务器加载launch文件是 ROS 启动多个节点的标准方式比开一堆终端敲rosrun优雅得多。这份源码里每个功能模块都有对应的.launch文件结构上分三层参数加载、节点启动、命名空间隔离。!-- launch/demo.launch 片段 -- launch !-- 加载 YAML 参数到参数服务器 -- rosparam file$(find robot_basics)/config/params.yaml commandload / !-- 启动节点指定命名空间和输出方式 -- node nametalker pkgrobot_basics typetalker_node outputscreen param namepublish_rate value10 / /node node namelistener pkgrobot_basics typelistener_node outputscreen / !-- 启动 RViz加载预设配置 -- node namerviz pkgrviz typerviz args-d $(find robot_basics)/config/demo.rviz / /launchrosparam把 YAML 文件里的键值对灌进参数服务器节点里用ros::param::get读。node的outputscreen让节点日志打到当前终端不加的话日志被吞进~/.ros/log调试时看不到ROS_INFO输出。param是节点私有的节点里用~publish_rate读和全局参数区分开。args传命令行参数给 RViz-d指定配置文件省得每次手动调视角。命名空间用group nsrobot1包起来多机器人场景下避免话题名冲突。这份源码里单机器人场景居多但结构上留了扩展余地。参数文件params.yaml里放的是可调参数比如发布频率、话题名、坐标系名称改行为不用重新编译改 YAML 重启 launch 就行。3. 核心通信机制话题、服务与动作的代码实现ROS 的通信机制是它的灵魂也是新手最容易迷糊的地方。话题Topic是异步广播服务Service是同步请求响应动作Action是带反馈的长任务。这份源码里三种都有示例我挑最核心的话题和服务把代码逐段拆开讲。3.1 话题发布与订阅talker/listener 完整链路话题通信是单向的发布者往话题上扔消息订阅者从话题上收。发布者和订阅者互相不知道对方存在靠话题名和消息类型匹配。这份源码里的talker.cpp和listener.cpp是最小可运行示例但麻雀虽小五脏俱全。// 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_node); ros::NodeHandle nh; // 创建发布者话题名 chatter队列长度 10 ros::Publisher pub nh.advertisestd_msgs::String(chatter, 10); // 发布频率 10Hz ros::Rate rate(10); int count 0; while (ros::ok()) { std_msgs::String msg; std::stringstream ss; ss hello world count; msg.data ss.str(); // 发布消息 pub.publish(msg); ROS_INFO(Published: %s, msg.data.c_str()); // 按频率休眠处理回调 rate.sleep(); } return 0; }ros::init的第三个参数是节点名同一个节点名不能同时跑两个否则 ROS 会杀掉先启动的那个。advertise的第二个参数是队列长度发布速度超过订阅者处理速度时消息在队列里排队满了就丢最旧的。ros::Rate控制循环频率rate.sleep()里会处理回调队列不调它的话订阅回调不执行。ros::ok()在收到CtrlC或rosnode kill时返回 false循环退出。订阅端对应的是listener.cpp// src/listener.cpp订阅者节点 #include ros/ros.h #include std_msgs/String.h // 回调函数收到消息时调用 void chatterCallback(const std_msgs::String::ConstPtr msg) { ROS_INFO(Received: %s, msg-data.c_str()); } int main(int argc, char** argv) { ros::init(argc, argv, listener_node); ros::NodeHandle nh; // 订阅话题 chatter队列长度 10注册回调 ros::Subscriber sub nh.subscribe(chatter, 10, chatterCallback); // 进入回调循环阻塞直到节点关闭 ros::spin(); return 0; }subscribe的第三个参数是回调函数指针消息到达时 ROS 自动调用。ros::spin()是个死循环不断检查回调队列并执行。如果节点里还有其他循环要跑用ros::spinOnce()放在循环里每次处理一轮回调。回调函数的参数是ConstPtr指向消息的常量指针别改成非 const编译能过但不符合规范。注意发布者和订阅者的话题名必须完全一致包括大小写和命名空间。/chatter和chatter在全局命名空间下是同一个话题但在节点命名空间下可能不同。用rostopic list确认话题名用rostopic echo /chatter看消息内容。3.2 服务端与客户端请求响应模式的参数传递服务是同步的客户端发请求服务端处理后返回响应。适合配置查询、模式切换这类短操作。这份源码里的服务示例是加法运算简单但把srv文件的定义、编译、调用全串起来了。先看srv/AddTwoInts.srv的定义# 请求部分 int64 a int64 b --- # 响应部分 int64 sum---上面是请求字段下面是响应字段。这个文件放在功能包的srv目录下package.xml里加build_dependmessage_generation/build_depend和exec_dependmessage_runtime/exec_dependCMakeLists.txt里加add_service_files(FILES AddTwoInts.srv)和generate_messages(DEPENDENCIES std_msgs)。编译后自动生成AddTwoInts.hC 里 include 就能用。服务端代码// src/add_server.cpp服务端 #include ros/ros.h #include robot_basics/AddTwoInts.h // 服务回调req 是请求res 是响应 bool addCallback(robot_basics::AddTwoInts::Request req, robot_basics::AddTwoInts::Response res) { res.sum req.a req.b; ROS_INFO(Request: %ld %ld %ld, req.a, req.b, res.sum); return true; // 返回 true 表示处理成功 } int main(int argc, char** argv) { ros::init(argc, argv, add_server_node); ros::NodeHandle nh; // 创建服务服务名 add_two_ints注册回调 ros::ServiceServer service nh.advertiseService(add_two_ints, addCallback); ROS_INFO(Add service ready.); ros::spin(); return 0; }回调返回true表示成功返回false客户端会收到失败标志。请求和响应的字段名来自.srv文件req.a、req.b、res.sum一一对应。服务名add_two_ints是客户端调用时的标识全局唯一。客户端代码// src/add_client.cpp客户端 #include ros/ros.h #include robot_basics/AddTwoInts.h int main(int argc, char** argv) { ros::init(argc, argv, add_client_node); ros::NodeHandle nh; // 创建客户端服务名必须和服务端一致 ros::ServiceClient client nh.serviceClientrobot_basics::AddTwoInts(add_two_ints); robot_basics::AddTwoInts srv; srv.request.a 10; srv.request.b 20; // 调用服务阻塞直到响应返回 if (client.call(srv)) { ROS_INFO(Sum: %ld, srv.response.sum); } else { ROS_ERROR(Failed to call service); } return 0; }client.call(srv)是阻塞调用服务端没响应就卡在这里。生产环境里要加超时用client.waitForExistence(ros::Duration(5.0))先等服务上线再call。服务端没启动时call直接返回 false不会一直等。服务名和话题名一样支持命名空间多机器人场景下用/robot1/add_two_ints区分。3.3 消息队列与回调处理spinOnce 与多线程ros::spin()是单线程的所有回调在一个线程里排队执行。如果某个回调耗时太长后面的回调全堵着。这份源码里有个传感器数据处理节点回调里做点云滤波耗时几十毫秒用spin()会导致控制指令延迟。解决办法是用ros::AsyncSpinner开多线程回调。// 多线程回调示例 #include ros/ros.h #include sensor_msgs/PointCloud2.h void cloudCallback(const sensor_msgs::PointCloud2::ConstPtr msg) { // 模拟耗时处理 ros::Duration(0.05).sleep(); ROS_INFO(Processed cloud with %d points, msg-width * msg-height); } int main(int argc, char** argv) { ros::init(argc, argv, cloud_processor); ros::NodeHandle nh; ros::Subscriber sub nh.subscribe(points, 1, cloudCallback); // 开 4 个线程处理回调 ros::AsyncSpinner spinner(4); spinner.start(); // 主线程做其他事或者等待 ros::waitForShutdown(); return 0; }AsyncSpinner的构造参数是线程数一般设成 CPU 核心数。spinner.start()后回调在独立线程里跑主线程用ros::waitForShutdown()阻塞。订阅队列长度设成 1点云数据量大队列长了内存暴涨丢帧比延迟好。多个订阅者共用一个 spinner回调之间可能并发共享数据要加锁。提示ros::spin()和ros::AsyncSpinner不能同时用。用了 AsyncSpinner 就别再调 spin()否则回调被执行两次。调试时用ros::spinOnce()手动控制回调时机比如在循环里处理完其他逻辑再处理回调。4. TF 坐标变换与传感器数据接入从 base_link 到 laser机器人开发绕不开坐标变换。激光雷达装在底盘上雷达坐标系叫laser底盘坐标系叫base_link导航算法要知道雷达数据在base_link下的位置就得靠 TF 树。这份源码里的 TF 示例把静态变换、动态变换、传感器数据接入串在一起是理解 ROS 坐标系管理的入口。4.1 TF 树构建静态变换与动态广播TF 树是一棵坐标系之间的变换关系树每个节点是一个坐标系边是变换矩阵。ROS 里用tf::TransformBroadcaster广播变换用tf::TransformListener查询变换。静态变换用static_transform_publisher命令行工具或 launch 文件里的node发布动态变换在代码里按周期广播。// src/tf_broadcaster.cpp动态广播 base_link 到 laser 的变换 #include ros/ros.h #include tf/transform_broadcaster.h int main(int argc, char** argv) { ros::init(argc, argv, tf_broadcaster_node); ros::NodeHandle nh; tf::TransformBroadcaster br; tf::Transform transform; ros::Rate rate(10); double angle 0.0; while (ros::ok()) { // 设置平移激光雷达在底盘前方 0.2m上方 0.1m transform.setOrigin(tf::Vector3(0.2, 0.0, 0.1)); // 设置旋转绕 Z 轴缓慢旋转模拟动态变化 tf::Quaternion q; q.setRPY(0, 0, angle); transform.setRotation(q); // 广播变换父坐标系 base_link子坐标系 laser br.sendTransform(tf::StampedTransform( transform, ros::Time::now(), base_link, laser)); angle 0.01; rate.sleep(); } return 0; }setOrigin设平移setRotation设旋转旋转用四元数表示setRPY把欧拉角转四元数。sendTransform的参数是StampedTransform带时间戳、父坐标系、子坐标系。时间戳用ros::Time::now()TF 查询时按时间戳找最近的变换。父坐标系和子坐标系不能搞反base_link是父laser是子表示从base_link到laser的变换。静态变换不用写代码launch 里一行搞定!-- 静态变换base_link 到 laser平移 (0.2, 0, 0.1)无旋转 -- node pkgtf typestatic_transform_publisher namebase_to_laser args0.2 0 0.1 0 0 0 base_link laser 100 /args依次是 x y z yaw pitch roll 父坐标系 子坐标系 发布周期(ms)。静态变换只发一次也行但有些版本的 TF 要求周期性发布设 100ms 稳妥。4.2 激光雷达数据接入sensor_msgs/LaserScan 解析激光雷达数据用sensor_msgs/LaserScan消息包含角度范围、距离数组、时间戳。这份源码里的雷达处理节点订阅/scan话题把极坐标下的距离数据转成笛卡尔坐标点再发布成PointCloud给后续处理。// src/laser_processor.cpp激光数据处理 #include ros/ros.h #include sensor_msgs/LaserScan.h #include sensor_msgs/PointCloud.h #include cmath ros::Publisher cloud_pub; void scanCallback(const sensor_msgs::LaserScan::ConstPtr scan) { sensor_msgs::PointCloud cloud; cloud.header scan-header; // 遍历距离数组转笛卡尔坐标 for (size_t i 0; i scan-ranges.size(); i) { float range scan-ranges[i]; // 过滤无效值inf 和 nan 跳过 if (!std::isfinite(range)) continue; if (range scan-range_min || range scan-range_max) continue; // 计算角度起始角 索引 * 角增量 float angle scan-angle_min i * scan-angle_increment; geometry_msgs::Point32 p; p.x range * cos(angle); p.y range * sin(angle); p.z 0.0; cloud.points.push_back(p); } cloud_pub.publish(cloud); ROS_INFO(Converted %zu points, cloud.points.size()); } int main(int argc, char** argv) { ros::init(argc, argv, laser_processor_node); ros::NodeHandle nh; cloud_pub nh.advertisesensor_msgs::PointCloud(laser_cloud, 1); ros::Subscriber sub nh.subscribe(scan, 1, scanCallback); ros::spin(); return 0; }ranges数组里存的是每个角度上的距离angle_min是起始角angle_increment是角分辨率。isfinite过滤掉inf超出量程和nan无效读数。range_min和range_max是雷达的有效量程超出范围的读数不可信。转换后的点云在laser坐标系下要转到base_link下得用 TF 变换tf::TransformListener的transformPointCloud方法一行搞定。注意激光雷达数据频率高10-50Hz回调里别做重活。点云转换这种计算放回调里没问题但滤波、配准这类耗时操作要么开多线程要么把数据扔给独立线程处理。队列长度设 1丢帧比延迟好。4.3 传感器时间同步message_filters 的使用多传感器融合时不同传感器的数据到达时间不一致直接拿最新数据配对可能差几十毫秒。message_filters提供时间同步机制按时间戳对齐两路或多路消息。这份源码里的视觉和雷达融合示例用了ApproximateTime策略。// src/sync_fusion.cpp激光与图像时间同步 #include ros/ros.h #include message_filters/subscriber.h #include message_filters/synchronizer.h #include message_filters/sync_policies/approximate_time.h #include sensor_msgs/LaserScan.h #include sensor_msgs/Image.h typedef message_filters::sync_policies::ApproximateTime sensor_msgs::LaserScan, sensor_msgs::Image SyncPolicy; void fusionCallback(const sensor_msgs::LaserScan::ConstPtr scan, const sensor_msgs::Image::ConstPtr image) { ROS_INFO(Synced: scan at %.3f, image at %.3f, scan-header.stamp.toSec(), image-header.stamp.toSec()); // 融合处理... } int main(int argc, char** argv) { ros::init(argc, argv, sync_fusion_node); ros::NodeHandle nh; // 消息过滤器订阅者 message_filters::Subscribersensor_msgs::LaserScan scan_sub(nh, scan, 1); message_filters::Subscribersensor_msgs::Image image_sub(nh, image, 1); // 近似时间同步器队列长度 10 message_filters::SynchronizerSyncPolicy sync(SyncPolicy(10), scan_sub, image_sub); sync.registerCallback(boost::bind(fusionCallback, _1, _2)); ros::spin(); return 0; }ApproximateTime策略允许时间戳有偏差按最近邻匹配。ExactTime要求时间戳完全一致实际中很难满足。队列长度 10 表示缓存 10 条消息等待配对太小会丢太大会延迟。registerCallback注册的回调参数顺序和SyncPolicy模板参数顺序一致。boost::bind把成员函数或普通函数绑成回调_1、_2是占位符。5. 避坑与排查ROS 开发中那些让人抓狂的瞬间ROS 的坑多且隐蔽报错信息经常指向错误的方向。这一章记录几个我实际踩过的坑按「现象 → 原因 → 解决」写希望能帮你省下几个小时的排查时间。5.1 编译报错找不到消息头文件现象catkin_make报fatal error: robot_basics/AddTwoInts.h: No such file or directory但srv/AddTwoInts.srv明明存在。原因CMakeLists.txt里漏了add_service_files或generate_messages或者catkin_package里没声明CATKIN_DEPENDS message_runtime。消息头文件是编译时自动生成的生成规则没配好就不会生成。解决检查CMakeLists.txt里add_service_files(FILES AddTwoInts.srv)和generate_messages(DEPENDENCIES std_msgs)是否都有package.xml里message_generation和message_runtime是否都声明了。改完删掉build和devel重新编译增量编译有时不重新生成消息。5.2 rosrun 找不到节点现象rosrun robot_basics talker_node报[rosrun] Couldnt find executable named talker_node但devel/lib/robot_basics/下确实有talker_node。原因新终端没 sourcedevel/setup.bash或者add_executable生成的可执行文件名和rosrun用的名字不一致。解决source devel/setup.bash后再试。如果还不行rospack find robot_basics确认包路径ls devel/lib/robot_basics/确认可执行文件名。add_executable(talker_node ...)的第一个参数就是rosrun用的名字别搞错。5.3 TF 报错 Lookup would require extrapolation into the future现象查询 TF 变换时报Lookup would require extrapolation into the future. Requested time 123.456 but the earliest data is at time 123.789。原因查询的时间戳比 TF 缓存里最新数据还新。常见于用ros::Time::now()查询但 TF 广播有延迟最新变换还没进缓存。解决查询时用ros::Time(0)表示「最新可用」或者等tf_listener.waitForTransform超时返回。waitForTransform会阻塞直到变换可用或超时比直接lookupTransform稳妥。5.4 节点启动后话题列表里没有预期话题现象rosnode list能看到节点但rostopic list里没有该节点发布的话题。原因节点在advertise之前就退出了或者advertise的代码在条件分支里没执行到。也可能是节点崩了但rosnode list还没更新。解决rosnode info /node_name看节点的发布和订阅列表。如果节点已死rosnode list里会显示但rosnode ping不通。在advertise后加ROS_INFO确认执行到。节点崩溃时看终端输出或~/.ros/log下的日志。5.5 参数服务器读取失败现象ros::param::get(~publish_rate, rate)返回 falserate保持默认值。原因launch 文件里param的命名空间和节点不匹配或者 YAML 文件没加载。~前缀表示节点私有参数launch 里param写在node标签内才是私有的。解决rosparam list看参数服务器里有哪些参数rosparam get /node_name/publish_rate看具体值。launch 里param写在node内或者用rosparam加载 YAML 时注意ns属性。参数名大小写敏感PublishRate和publish_rate是两个参数。6. 二次开发上手改参数、加节点、扩功能源码包的价值在于能改。这份源码的结构留了扩展余地改参数不用重新编译加节点照着现有模板抄扩功能从消息定义开始。下面按「改什么 → 怎么改 → 怎么验证」的顺序讲几个实际开发中高频的改造场景。6.1 参数化配置不改代码调整行为最安全的改造是改参数。这份源码里所有可调项都抽到了 YAML 文件里比如发布频率、话题名、坐标系名称、滤波阈值。改 YAML 重启 launch 就行不用碰 C 代码。# config/params.yaml talker: publish_rate: 20 # 发布频率从 10Hz 改成 20Hz topic_name: chatter # 话题名改这个要同步改订阅端 laser: range_min: 0.1 # 雷达最小有效距离 range_max: 10.0 # 雷达最大有效距离 filter_enabled: true # 是否开启滤波 tf: base_frame: base_link laser_frame: laser publish_period: 100 # 静态变换发布周期(ms)节点里用ros::param::get读带默认值double publish_rate; nh.paramdouble(~publish_rate, publish_rate, 10.0); // 默认 10Hz ros::Rate rate(publish_rate);nh.param比ros::param::get多一个默认值参数读不到就用默认值不会因为参数缺失崩溃。~前缀读节点私有参数launch 里param写在node内。改完参数用rosparam get /node_name/publish_rate确认生效。6.2 新增自定义节点从模板到编译加新节点分四步写源文件、改CMakeLists.txt、改package.xml如果需要新依赖、编译测试。源文件照着现有节点抄改节点名、话题名、回调逻辑。// src/my_node.cpp自定义节点模板 #include ros/ros.h #include std_msgs/Float64.h ros::Publisher pub; void inputCallback(const std_msgs::Float64::ConstPtr msg) { std_msgs::Float64 out; out.data msg-data * 2.0; // 自定义处理逻辑 pub.publish(out); } int main(int argc, char** argv) { ros::init(argc, argv, my_node); ros::NodeHandle nh; pub nh.advertisestd_msgs::Float64(output, 10); ros::Subscriber sub nh.subscribe(input, 10, inputCallback); ros::spin(); return 0; }CMakeLists.txt里加两行add_executable(my_node src/my_node.cpp) target_link_libraries(my_node ${catkin_LIBRARIES})package.xml里如果用了新的消息类型加对应的build_depend和exec_depend。编译catkin_make跑rosrun robot_basics my_node。验证时开三个终端一个roscore一个rostopic pub /input std_msgs/Float64 data: 5.0一个rostopic echo /output看输出是不是 10.0。6.3 扩展消息类型与动作接口内置消息类型不够用时自定义消息。在功能包里建msg/MyMessage.msgpackage.xml加message_generation和message_runtimeCMakeLists.txt加add_message_files(FILES MyMessage.msg)和generate_messages(DEPENDENCIES std_msgs)。编译后生成MyMessage.hinclude 就能用。动作Action比服务多一个反馈通道适合导航、抓取这类长任务。定义.action文件包含目标、结果、反馈三部分# action/MoveTo.action geometry_msgs/PoseStamped target_pose --- bool success --- geometry_msgs/PoseStamped current_poseCMakeLists.txt里add_action_files(FILES MoveTo.action)generate_messages加actionlib_msgs依赖。动作服务端用actionlib::SimpleActionServer客户端用SimpleActionClient反馈用publishFeedback发。这套机制比服务灵活但代码量大简单场景用服务就够了。6.4 验证改造效果用 rostopic 和 rqt 快速确认改完代码别急着上真机先用命令行工具验证。rostopic hz /topic看发布频率rostopic bw /topic看带宽rostopic echo /topic看内容。rqt_graph看节点和话题的连接关系rqt_plot画数值曲线rqt_console看日志。这些工具比 RViz 轻量调试通信问题时先用它们定位。# 常用验证命令 rostopic list # 列出所有话题 rostopic hz /chatter # 查看发布频率 rostopic echo /chatter -n 5 # 查看最近 5 条消息 rosnode info /talker_node # 查看节点详情 rqt_graph # 可视化节点连接改完参数后rosparam get /talker/publish_rate确认参数生效rostopic hz /chatter确认频率变了。改完代码后rosrun启动节点rosnode info看发布订阅列表是否符合预期。真机测试前用rosbag record录一段数据rosbag play回放确保算法在离线数据上跑通再上硬件。从那以后我每次改完 ROS 代码都强制走一遍「编译 → source → rosrun → rostopic echo」的流程确认话题通了再往下做。这套流程看着笨但省下的排查时间远超那几行命令的开销。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑