拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ROS1框架

ROS1框架

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/odom

EGO 有两套:

/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.cpp

package.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 计算路径 发布 trajectory

include/

格式通常是:

.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/reset

Client:

“请清空地图”

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 B

C 会收到两边的数据。

所以 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/data

launch 中:

<remap from="/imu/data" to="/uav1/imu/data"/>

不用改 C++。


你现在其实已经用了 remap。

例如 EGO 内部可能订阅:

/grid_map/odom

你在 launch 里 remap 成:

/vins_estimator/odometry

这就是:

程序内部接口 ↓ remap 实际系统topic

ROS1 运行时真正的“世界观”

你以后可以完全这样理解:

磁盘结构: 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 系统排错最重要的命令之一。

返回列表