ROS1 里真正区分“是不是串台”的,不是“项目一/项目二”这个概念,而是当前 ROS Master 里有哪些节点在发布哪些 topic。
如果项目一和项目二恰好都使用:
/camera/image而且消息类型都一样,例如:
sensor_msgs/Image那么只要它们同时连到同一个 ROS Master,并且都在发布这个 topic,订阅者就会同时收到两个 publisher 的消息。ROS 不会自动知道“这个属于项目一,那个属于项目二”。
所以确实有两种情况:
项目一没启动 → 不会串 项目一和项目二同时启动,而且 topic 重名 → 会混在一起更准确地说,ROS 里 topic 是全局通信名字,不是“绑定某个工程”的私有名字。
比如:
节点A(项目一) 发布 /odom 节点B(项目二) 也发布 /odom 节点C 订阅 /odom那么节点C会收到A和B两个 publisher 的消息。
这就是为什么 ROS 工程里很重视:
namespace remap node name topic name来避免冲突。
namespace是最常用的隔离方法。
比如你有两架无人机:
无人机1 无人机2如果都用:
/odom就会冲突。
所以可以设计成:
/uav1/odom /uav2/odom这就是 namespace 的作用。
launch 里可以这么写:
<group ns="uav1"> <node pkg="my_pkg" type="odom_node" name="odom_node"/> </group>如果程序内部发布的是相对 topic:
odom那么最终会变成:
/uav1/odom另一组:
<group ns="uav2"> <node pkg="my_pkg" type="odom_node" name="odom_node"/> </group>最终就是:
/uav2/odom于是:
无人机1 → /uav1/odom 无人机2 → /uav2/odom互不干扰。
remap则是“改接口名字”。
比如 EGO 源码里写死订阅:
/grid_map/odom但你实际想让它用:
/vins_estimator/odometry你不想改 C++ 源码,就在 launch 里写:
<remap from="/grid_map/odom" to="/vins_estimator/odometry"/>意思就是:
EGO内部以为自己订阅: /grid_map/odom ROS实际给它接到: /vins_estimator/odometry这就是 remap。
你现在已经真实用过这个机制。
再举一个更完整的例子。
假设你同时跑“真实系统”和“仿真系统”。
真实系统:
节点: /vins_estimator 发布: /real/odom仿真系统:
节点: /simulator 发布: /sim/odomEGO 有两套:
/real/ego_planner /sim/ego_planner那么 launch 可以设计成:
<group ns="real"> <node pkg="ego_planner" type="ego_planner_node" name="ego_planner"> <remap from="/grid_map/odom" to="/vins_estimator/odometry"/> </node> </group>另一套:
<group ns="sim"> <node pkg="ego_planner" type="ego_planner_node" name="ego_planner"> <remap from="/grid_map/odom" to="/simulator/odometry"/> </node> </group>最终运行时就可以区分:
/real/ego_planner /sim/ego_planner以及:
真实: /vins_estimator/odometry 仿真: /simulator/odometry这样系统就非常清晰。
你可以这样理解:
项目/工作空间 只是代码放在哪里 真正运行时 ROS只关心: 谁是 node 谁发布哪个 topic 谁订阅哪个 topic 消息类型是什么所以运行时结构和磁盘目录结构是两回事。
一、先讲一个“最简单的 ROS1 项目”应该有什么
我们先假设你要做一个最简单的功能:
每秒打印一句 “hello ros”。
工作空间可以是:
~/catkin_ws目录:
catkin_ws/ ├── src/ │ └── hello_ros/ │ ├── CMakeLists.txt │ ├── package.xml │ └── src/ │ └── hello_node.cpp ├── build/ └── devel/最核心的其实就三个东西:
package.xml CMakeLists.txt src/hello_node.cpppackage.xml
格式:XML。
作用:描述这个 package 是谁、依赖谁。
例如:
<package format="2"> <name>hello_ros</name> <version>0.0.0</version> <description>Simple ROS package</description> <maintainer email="lzx@example.com">lzx</maintainer> <license>MIT</license> <buildtool_depend>catkin</buildtool_depend> <depend>roscpp</depend> <depend>std_msgs</depend> </package>你可以把它理解为:
功能包身份证 + 依赖清单它告诉 ROS:
我的名字: hello_ros 我需要: roscpp std_msgs它不是用来“列出所有子文件”的。
CMakeLists.txt
格式:CMake 脚本。
作用:告诉编译系统:
要找哪些依赖 哪些 cpp 要编译 编译成什么可执行程序 链接哪些库一个最简单例子:
cmake_minimum_required(VERSION 3.0.2) project(hello_ros) find_package(catkin REQUIRED COMPONENTS roscpp std_msgs ) catkin_package() include_directories( ${catkin_INCLUDE_DIRS} ) add_executable(hello_node src/hello_node.cpp) target_link_libraries( hello_node ${catkin_LIBRARIES} )可以把它理解成:
“编译说明书”src/hello_node.cpp
格式:C++ 源码。
作用:真正实现功能。
例如:
#include <ros/ros.h> int main(int argc, char **argv) { ros::init(argc, argv, "hello_node"); ros::NodeHandle nh; ros::Rate rate(1); while (ros::ok()) { ROS_INFO("hello ros"); rate.sleep(); } return 0; }这才是真正执行逻辑的程序。
编译以后,ROS 会生成一个可执行节点。
所以:
.cpp ↓ 编译 可执行程序 ↓ rosrun / roslaunch Node最简单 ROS1 package 的逻辑关系
可以记成:
package.xml → 我是谁,我依赖谁 CMakeLists.txt → 怎么编译我 src/*.cpp → 我真正干什么这三个就是一个最基础 C++ ROS package 的核心。
二、一个典型 ROS1 package 里面会有什么
真实项目一般会复杂很多,例如:
my_robot/ ├── package.xml ├── CMakeLists.txt │ ├── src/ │ ├── camera_node.cpp │ ├── localization_node.cpp │ └── planner_node.cpp │ ├── include/ │ └── my_robot/ │ ├── planner.h │ └── localization.h │ ├── launch/ │ ├── robot.launch │ ├── sensor.launch │ └── planner.launch │ ├── config/ │ ├── camera.yaml │ ├── vins.yaml │ └── planner.yaml │ ├── scripts/ │ └── imu_convert.py │ ├── msg/ │ └── Target.msg │ ├── srv/ │ └── Reset.srv │ ├── action/ │ └── Move.action │ ├── rviz/ │ └── robot.rviz │ └── urdf/ └── robot.urdf下面分别解释。
src/
格式通常是:
.cpp .c作用:
真正的 C/C++ 程序源码。
例如:
planner_node.cpp可能做:
订阅 odometry 订阅 depth 计算路径 发布 trajectoryinclude/
格式通常是:
.h .hpp作用:
C++ 头文件。
例如:
class Planner { public: void plan(); };常见结构:
include/ └── package_name/ └── planner.h这个目录主要是给:
函数声明 类定义 模板 公共接口用的。
launch/
格式:
.launch本质上是 XML。
作用:
批量启动节点,并且设置参数、remap、namespace。
例如:
<launch> <node pkg="my_robot" type="planner_node" name="planner"/> </launch>它不是程序逻辑本身。
它更像:
“启动编排文件”比如你现在:
run_with_d435i.launch就是在做:
启动 ego_planner_node 启动 traj_server 启动 waypoint_generator 设置 topic remap 设置参数config/
通常放:
.yaml .yml作用:
保存运行参数。
例如:
max_velocity: 2.0 max_acceleration: 3.0 odom_topic: "/vins_estimator/odometry"yaml 是:
参数表不是程序。
程序启动时读取它。
scripts/
通常放:
.py .sh例如:
imu_optical_to_body.py你已经在用了。
作用:
Python 节点、辅助脚本、工具程序。
例如 Python ROS node:
#!/usr/bin/env python3import rospyrospy.init_node("imu_converter")msg/
格式:
.msg作用:
自定义 ROS 消息。
例如:
Target.msg内容:
float64 x float64 y float64 z string name编译以后,就可以在代码里用:
my_robot::Target类似于你平时看到:
sensor_msgs/Image nav_msgs/Odometry geometry_msgs/Pose这些其实都是别的 package 定义好的消息。
srv/
格式:
.srv作用:
定义 ROS Service。
例如:
ResetMap.srv内容可能:
bool force --- bool success string message---上面是请求,下面是响应。
它适合:
请求一次 返回一次例如:
清空地图 重新初始化 切换模式action/
格式:
.action作用:
定义长时间任务。
例如:
飞到目标点 机械臂运动 导航到目标它比 service 多:
Goal Feedback Result适合耗时任务。
rviz/
通常:
.rviz作用:
保存 RViz 显示配置。
包括:
Fixed Frame Display列表 Topic 颜色 点大小 视角你刚刚保存的:
d435i_real.rviz就是这种。
urdf/
通常:
.urdf .xacro作用:
描述机器人结构。
包括:
机体 相机 IMU 激光雷达 轮子 各坐标系之间的位置关系比如:
base_link camera_link imu_link都可以在 URDF 里定义。
一个典型 package 可以这样记
package.xml → 身份和依赖 CMakeLists.txt → 编译规则 src/ → C++程序 include/ → 头文件 launch/ → 启动编排 config/ → 参数 scripts/ → Python / shell脚本 msg/ → 自定义消息 srv/ → 请求-响应 action/ → 长任务 rviz/ → RViz显示配置 urdf/ → 机器人结构和坐标系不是每个 package 都必须有全部这些目录。
三、一个完整 ROS1 workspace 里面可以有很多 package
例如:
uav_ws/ ├── src/ │ │ ├── camera_driver/ │ ├── imu_driver/ │ ├── vins/ │ ├── mapping/ │ ├── ego_planner/ │ ├── controller/ │ └── visualization/ │ ├── build/ └── devel/这些 package 可以互相通信。
真正运行时,大概是:
camera_driver ↓ /camera/image imu_driver ↓ /imu/data ↓ VINS ↓ /vins/odometry ↓ mapping ↓ /map ↓ ego_planner ↓ /trajectory ↓ controller所以 ROS 的核心思想其实不是:
一个大程序而是:
很多小节点 + 通过通信拼成系统四、不同功能包之间怎么通信
ROS1 主要有三种机制:
Topic Service Action最重要的是 Topic。
1. Topic:连续数据流
例如:
camera package 发布: /camera/image另外一个 package:
vision package 订阅: /camera/image只要满足:
Topic名字一样 消息类型一样 同一个ROS Master就能通信。
例如 Publisher:
ros::Publisher pub = nh.advertise<sensor_msgs::Image>( "/camera/image", 10 );Subscriber:
ros::Subscriber sub = nh.subscribe( "/camera/image", 10, imageCallback );它们不需要:
#include 对方的 cpp 调用对方函数 知道对方在哪个 workspace完全不需要。
只认:
topic message type ROS master这就是 ROS 的解耦。
2. Service:一次请求,一次回答
例如:
/map/resetClient:
“请清空地图”Server:
“清空成功”像:
问问题 → 等答案适合:
重置 开关设备 获取一次数据3. Action:长任务
例如:
“飞到坐标(10,5,2)”不能只用 service,因为执行可能要 20 秒。
Action 可以:
发目标 ↓ 不断反馈进度 ↓ 最后返回结果例如:
Goal: 飞到目标 Feedback: 已经完成 30% Result: 到达五、不同 package 之间怎么“引用”依赖
这里要区分两种:
情况A:只通过 Topic 通信
例如:
camera package → /camera/image → planner package这种情况下它们甚至可以完全不知道彼此 package 名。
只要:
topic名 消息类型一致即可。
情况B:package A 用到了 package B 定义的消息/库
例如:
#include <my_msgs/Target.h>那 A 就必须声明依赖 B。
package.xml:
<depend>my_msgs</depend>CMakeLists.txt:
find_package(catkin REQUIRED COMPONENTS roscpp my_msgs )这样编译系统才知道去哪里找。
六、你最关心的“串台”问题
假设项目一:
Node A 发布: /imu/data 类型: sensor_msgs/Imu项目二:
Node B 也发布: /imu/data 类型: sensor_msgs/Imu现在另一个节点:
Node C 订阅: /imu/data如果 A、B 同时运行:
Node C ↑ /imu/data ↑ ↑ Node A Node BC 会收到两边的数据。
所以 ROS 不会说:
这是项目1的 这是项目2的ROS 根本没有“项目”这个运行时概念。
怎么避免
最常用的是 namespace。
比如:
项目1: /uav1/imu/data 项目2: /uav2/imu/data这样就分开了。
launch 里可以:
<group ns="uav1"> ... </group>就会自动加前缀。
例如:
原来: /imu/data 进入 namespace uav1 后: /uav1/imu/data也可以 remap。
比如程序内部写死:
/imu/datalaunch 中:
<remap from="/imu/data" to="/uav1/imu/data"/>不用改 C++。
你现在其实已经用了 remap。
例如 EGO 内部可能订阅:
/grid_map/odom你在 launch 里 remap 成:
/vins_estimator/odometry这就是:
程序内部接口 ↓ remap 实际系统topicROS1 运行时真正的“世界观”
你以后可以完全这样理解:
磁盘结构: Workspace → Package → 文件 运行结构: Node → Topic → Service → Action它们不是一回事。
源码在:
package里。
一旦运行起来,ROS Master 看到的是:
Node A Node B Node C Topic X Topic Y Topic Z而不是:
这是workspace1 这是workspace2所以你现在排查系统时,经常用:
rosnode list因为是在看:
现在谁真的运行着用:
rostopic list是在看:
现在有哪些通信通道用:
rostopic info /xxx是在看:
谁发布 谁订阅这三条其实就是 ROS1 系统排错最重要的命令之一。