ROS 2 核心概念教程
目 录
声明:
本文内容整理自【古月居】古月·ROS2入门21讲和自己的学习笔记
1. 引言
机器人操作系统(ROS,Robot Operating System)是一个用于机器人软件开发的开源框架。ROS 2 是 ROS 的升级版本,提供了更好的性能和灵活性,适用于多种机器人应用场景。
2. 系统架构
2.1 ROS 和 ROS 2 是什么
Robot Operating System,简称为ROS,由通信机制、开发工具、应用功能、生态系统四大部分组成。
ROS 是一个用于机器人软件开发的开源框架,旨在提高机器人软件的复用性。ROS 2 是其升级版本,提供了更好的性能和灵活性,适用于多种机器人应用场景。
ROS也可以跨平台使用,Linux、Windows、嵌入式系统都可以跑。
ROS全球社区有几个重要网站:
| 网站 | 描述 |
|---|---|
| answers.ros.org | 一个ROS问答网站,大家可以在上边提出任何关于ROS的问题,全球很多开发者都很乐意回答我们的问题。 |
| wiki.ros.org | ROS的维基百科,记录了ROS教程和各种功能包的使用。 |
| discourse.ros.org | ROS论坛,关于ROS开发的新鲜事都可以在这里发表和查看,比如ROS的活动、新功能包的发布等等。 |
| index.ros.org | ROS各种资源的一个索引网站。 |
| packages.ros.org | ROS功能包存储的数据库。 |
2.2 ROS 2 对比 ROS 1
1. 发展历程

2. ROS 1 的特点和问题
ROS设计之初 是为了开发一款PR2家庭服务机器人,这款机器人绝大部分时间都是独立工作。具备了以下特点:
- 工作站级别的计算平台,支持各种复杂的实时运算和处理
- 良好的网络连接,没有丢数据或者黑客入侵的风险
- 高昂的成本和售价
当前问题:
- 要在资源有限的嵌入式系统中运行;
- 要在有干扰的地方保证通信的可靠性;
- 要做成产品走向市场,甚至用在自动驾驶汽车和航天机器人上。
当前需求:
- 多机器人系统:机器人和机器人之间需要通信和协作
- 跨平台:机器人应用场景不同,控制平台也会有差异
- 实时性:机器人运动控制和很多行为策略要求机器人具备实时性
- 网络连接:无论在怎样的网络环境下,ROS 2都可以尽量保障机器人大量数据的完整性和安全性
- 产品化:不仅可以用于机器人研发阶段,还可以直接搭载在产品中,走向消费市场,这对ROS 2的稳定性、强壮性也提除了巨大挑战。
- 项目管理:机器人开发是一个复杂的系统工程,设计、开发、调试、测试、部署等全流程的项目管理工具和机制,也会在ROS 2中体现,更方便我们去开发一款机器人。
ROS 2的变化:
| 架构的颠覆 | ROS 1架构下,所有节点需要Master进行管理,若Master出现问题,系统就面临宕机的风险 |
| ROS 2使用基于DDS的Discovery机制 | |
| API的重新设计 | ROS 2重新设计了API,且使用方法类似于ROS 1 |
| 编译系统升级 | ROS 1使用rosbuild、catlin管理项目 |
| ROS 2使用ament、colcon |

2.3 ROS 2 安装方法
1. Linux系统简介
Linux是一套免费并且开放源代码的操作系统,是ROS2依赖的重要底层系统,对Linux系统的支持最好
Linux应该叫做操作系统内核,并没有可视化界面,发行版就是给这个内核加上华丽的外衣,把操作界面和各种应用软件放到一起,打包成我们安装系统的镜像。每一个发行版都有其适用的场景,比如RedHat适合商业应用、CentOS适合服务器、Ubuntu、Fedora适合个人使用等

2. 不同安装方法对比

3. 基于虚拟机的Ubuntu安装方法
-
虚拟机安装
https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html -
下载系统镜像
https://ubuntu.com/download/desktop选择带有LTS的版本(长期支持版:5年持续维护更新),其余只有18个月。
-
创建系统
(1)虚拟机安装方法:
参考教程:https://zhuanlan.zhihu.com/p/684460783 https://blog.csdn.net/qq_42417071/article/details/136327674相关设置:



(2)双系统的安装和一些配置可以参考博主的另一篇文章:
https://blog.csdn.net/Master_Zhang_/article/details/147670342?spm=1001.2014.3001.5501
-
安装ROS2
wget http://fishros.com/install -O fishros && . fishros参考链接:动手安装ROS2
查看安装位置:
cd /opt/ros/humble/ ls卸载ROS2
sudo apt remove ros-humble-* sudo apt autoremove -
Ubuntu系统常用设置
分辨率
本显示器配置: 1920*1080 16:9 75Hz 125%缩放 虚拟机分辨率设置: -半屏:1280*1024(5:4) 60Hz 125%缩放 -全屏:1920*1080(16:9) 60Hz 125%缩放
2.4 命令行操作
启动终端
在 Ubuntu 系统中,终端是通过字符输入进行命令操作的重要工具。启动终端的方式如下:
-
方式 1: 在应用列表中打开。
-
方式 2: 使用快捷键
Ctrl+Alt+T -
方式 3: 鼠标右键选择“打开终端”。
Linux 常用命令总结
| 命令 | 语法 | 功能说明 |
|---|---|---|
cd | cd <目录路径> | 改变工作目录,未指定路径时回到用户主目录。 |
pwd | pwd | 显示当前工作目录的绝对路径。 |
mkdir | mkdir [选项] <目录名称> | 创建一个目录/文件夹。 |
touch | touch <文件名称> | 创建一个空文件。 |
nano | nano <文件名称> | 使用 nano 编辑器编辑文件。 |
ls | ls [选项] [目录名称...] | 列出目录/文件夹中的文件列表。 |
gedit | gedit <文件名称> | 打开 gedit 编辑器编辑文件,若文件不存在则新建。 |
mv | mv [选项] <源文件或目录> <目标> | 重命名文件或将文件移动到目标目录。 |
cp | cp [选项] <源文件> <目标文件> | 复制文件或目录到指定目标。 |
rm | rm [选项] <文件或目录> | 删除文件或目录,可递归删除子目录内容。 |
sudo | sudo [选项] [指令] | 以管理员权限执行命令。 |
source | source <文件路径> | 在当前 shell 环境中执行文件中的命令。 |
echo | echo [选项] [字符串] | 输出字符串到终端或文件。 |
export | export <变量名>=<值> | 设置环境变量。 |
grep | grep [选项] <模式> <文件> | 在文件中搜索匹配的模式。 |
find | find <路径> [选项] [表达式] | 在指定目录下查找文件。 |
chmod | chmod [选项] <模式> <文件> | 更改文件或目录的权限。 |
chown | chown [选项] <所有者> <文件> | 更改文件或目录的所有者。 |
示例:
- 创建文件夹
mkdir -p ~/try_ws/src
cd ~/try_ws/src
ros2 pkg create robot_description --build-type ament_cmake --license Apache-2.0
- 创建文件
gedit ~/try_ws/src/robot_description/CMakeLists.txt
ROS2 命令行操作指南
ROS2 的命令行机制与 Linux 类似,但所有操作集成在 ros2 命令下。以下是 ROS2 常用命令及功能。通过 -h 或 --help 的方式查看命令帮助文档。通过 Tab 键可以查看命令的自动补全提示。
ros2 pkg:功能包管理工具
ros2 run:运行功能包节点程序
ros2 node:节点相关命令行工具
ros2 topic:话题通信相关的命令行工具
ros2 interface:接口(msg、srv、action)消息相关的命令行工具ros2 service:服务通信相关的命令行工具
ros2 action:动作通信相关的命令行工具ros2 param:参数服务相关的命令行工具
2.5 核心概念
功能包
ros2 pkg create --build-type <编译类型> <pkg_name> <依赖1> <依赖2> # 创建功能包
# 编译类型:ament_cmake、ament_python
ros2 pkg list # 列出所有功能包
ros2 pkg describe <package_name> # 查看功能包的详细信息
ros2 pkg executables <package_name> # 查看功能包的可执行文件
示例:
ros2 pkg create --build-type ament_python pkg_name rclpy std_msgs sensor_msgs
节点
ros2 run <pkg_name> <node_name> # 运行指定功能包的节点
ros2 node list # 列出所有节点
ros2 node info /turtlesim # 查看指定节点的详细信息
示例:
ros2 run turtlesim turtlesim_node # 运行海龟仿真节点
ros2 run turtlesim turtle_teleop_key # 运行键盘控制节点
话题
查看当前系统中的话题:
ros2 topic bw <topic_name> # 查看话题的带宽
ros2 topic delay <topic_name> # 查看话题的延迟
ros2 topic echo <topic_name> # 查看话题的消息数据
ros2 topic find <type_name> # 查找特定类型的话题
ros2 topic hz <topic_name> # 查看话题的发布频率
ros2 topic info <topic_name> # 查看话题的详细信息
ros2 topic list # 列出所有话题
ros2 topic type <topic_name> # 查看话题的接口类型
ros2 topic pub <topic_name> <msg_type> <msg_data> # 发布话题消息
示例:
ros2 topic list -t # 查看所有话题的类型
ros2 topic info /turtle1/cmd_vel # 查看指定话题的详细信息
ros2 topic echo /turtle1/pose # 查看指定话题的消息数据
ros2 topic pub --rate 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}" #通过话题指令直接控制海龟移动:
服务
ros2 service list # 查看服务列表
ros2 service type <service_name> # 查看服务数据类型
ros2 service find <type_name> # 查找特定类型的服务
ros2 service call <service_name> <service_type> <service_data> # 发送服务请求
动作
ros2 action list # 查看服务列表
ros2 action info <action_name> # 查看服务数据类型
ros2 action send_goal <action_name> <action_type> <action_data> # 发送服务请求,yaml格式
示例:
ros2 interface show turtlesim/action/RotateAbsolute # 接口展示
接口
ros2 interface list # 查看系统接口列表
ros2 interface show <interface_name> # 查看某个接口的详细定义
ros2 interface package <package_name> # 查看某个功能包中的接口定义
ros2 interface packages # 查看包含接口消息的功能包
ros2 interface proto <interface_name> # 查看某个接口消息原型
参数
-
查看参数列表:
ros2 param list -
参数查询与修改:
ros2 param describe <node_name> <parameter_name> # 查看某个参数的描述信息 ros2 param get <node_name> <parameter_name> <value> # 查询某个参数的值 ros2 param set <node_name> <parameter_name> <value> # 修改某个参数的值 ros2 param delete <node_name> <parameter_name> # 删除某个参数 -
参数文件保存与加载:
#查看节点当前的所有参数值 ros2 param dump <node_name> #查看节点当前的所有参数值 ros2 param dump <node_name> > <parameter_file> #从文件加载参数: ros2 param load <node_name> <parameter_file> #通常为yaml文件 #使用保存的参数值启动同一节点 ros2 run <package_name> <executable_name> --ros-args --params-file <file_name> #例如: ros2 run turtlesim turtlesim_node --ros-args --params-file turtlesim.yaml
编译
colcon build # 编译工作空间中的所有功能包
colcon build --packages-select <package_1> <package_2> # 编译指定功能包
其它
录制与播放控制命令
使用 rosbag 录制与回放 ROS2 数据:
ros2 bag record /turtle1/cmd_vel # 录制话题数据
ros2 bag play <rosbag文件路径> # 播放录制的数据
2.6 ROS 2 开发环境配置
1. 安装git
sudo apt install git #安装git
git clone https://gitee.com/guyuehome/ros2_21_tutorials.git #下载教程
2. VSCode 插件配置指南
推荐插件列表
| 插件名称 | 功能说明 |
|---|---|
| 中文语言包 | 为 VSCode 提供中文支持,方便中文用户使用。 |
| Python 插件 | 提供 Python 语言支持,包括代码补全、调试、Linting 等功能,适用于 Python 开发。 |
| C++ 插件 | 提供 C++ 语言支持,包括语法高亮、调试、代码补全等功能。 |
| CMake 插件 | 提供 CMake 语法支持,用于管理和构建 C++ 项目。 |
| vscode-icons | 添加美观的文件图标,便于快速识别不同类型的文件。 |
| ROS 插件 | 专为 ROS/ROS2 开发设计的插件,支持启动文件、话题、服务的可视化与管理。 |
| Msg Language Support | 为 ROS 消息文件(.msg)和服务文件(.srv)提供语法高亮与支持。 |
| Visual Studio IntelliCode | 使用 AI 提供智能代码补全建议,提高编程效率。 |
| URDF 插件 | 为 ROS 中的 URDF 文件提供语法支持,便于编辑机器人描述文件。 |
| Markdown All in One | 提供 Markdown 文件支持,包括语法高亮、预览、表格生成等功能,适用于文档书写与管理。 |
vscode常用命令
| 功能 | 快捷键 |
|---|---|
| 新建终端 | Ctrl+Shift+ |
| 打开终端 | `Ctrl+`` |
| 关闭终端 | Ctrl+Shift+W |
| 切换终端 | Ctrl+PageUp/PageDown |
| 查找 | Ctrl+F |
| 替换 | Ctrl+H |
| 全局查找 | Ctrl+Shift+F |
| 全局替换 | Ctrl+Shift+H |
| 打开命令面板 | Ctrl+Shift+P |
| 打开设置 | Ctrl+, |
| 打开扩展 | Ctrl+Shift+X |
| 格式化代码 | Shift+Alt+F |
| 代码折叠 | Ctrl+Shift+[ |
| 代码展开 | Ctrl+Shift+] |
| 注释代码 | Ctrl+/ |
| 多行注释 | Shift+Alt+A |
| 跳转到定义 | F12 |
| 查找所有引用 | Shift+F12 |
| 重命名符号 | F2 |
| 显示大纲 | Ctrl+Shift+O |
| 显示问题 | Ctrl+Shift+M |
| 显示调试控制台 | Ctrl+Shift+Y |
| 启动调试 | F5 |
| 停止调试 | Shift+F5 |
| 单步调试 | F10 |
| 进入函数 | F11 |
| 跳出函数 | Shift+F11 |
| 切换全屏 | F11 |
| 切换 Zen 模式 | Ctrl+K Z |
| 切换侧边栏 | Ctrl+B |
| 切换活动栏 | Ctrl+Shift+B |
| 切换面板 | Ctrl+J |
| 切换编辑器布局 | Ctrl+Shift+P,输入 View: Toggle Editor Group Layout |
| 切换文件图标主题 | Ctrl+Shift+P,输入 Preferences: File Icon Theme |
| 切换颜色主题 | Ctrl+Shift+P,输入 Preferences: Color Theme |
| 切换键盘快捷键 | Ctrl+Shift+P,输入 Preferences: Open Keyboard Shortcuts |
| 切换设置同步 | Ctrl+Shift+P,输入 Preferences: Turn On Settings Sync |
| 切换工作区 | Ctrl+Shift+P,输入 Workspaces: Open Workspace |
| 切换窗口 | Ctrl+Shift+P,输入 View: Switch Window |
| 切换活动栏 | Ctrl+Shift+B |
| 切换面板 | Ctrl+J |
3. 常用python包的安装
# pip源设置
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# pip源查看
pip config list
#安装opencv
sudo apt install python3-opencv
#安装三维几何变换的包
sudo pip3 install transforms3d
# 语音合成
pip install espeakng
# 资源监视器
pip install nvitop
4. 常用ROS包,软件,依赖的安装
export ROS_DISTRO=humble
echo $ROS_DISTRO
#安装ros相机驱动
sudo apt install ros-humble-usb-cam
#安装turtle的tf工具
sudo apt install ros-humble-turtle-tf2-py ros-humble-tf2-tools
sudo apt install ros-humble-tf-transformations
#安装gazebo相关的所有包
sudo apt install ros-humble-gazebo-*
#安装rqt(humble版本)
sudo apt install ros-humble-rqt
#显示文件夹结构
sudo apt install tree
#
sudo apt install net-tools
sudo apt install ros-$ROS_DISTRO-joint-state-publisher-gui ros-$ROS_DISTRO-robot-state-publisher
sudo apt install ros-$ROS_DISTRO-xacro
sudo apt install ros-$ROS_DISTRO-gazebo-ros-pkgs
#控制器插件
sudo apt install ros-$ROS_DISTRO-ros2-control ros-$ROS_DISTRO-ros2-controllers
sudo apt install ros-$ROS_DISTRO-gazebo-ros2-control
#安装
sudo apt-get install ros-$ROS_DISTRO-navigation2 #安装导航包
sudo apt-get install ros-$ROS_DISTRO-nav2-bringup #安装启动包
#安装slam_toolbox
sudo apt-get install ros-$ROS_DISTRO-slam-toolbox
sudo apt-get install ros-$ROS_DISTRO-nav2-map-server
#语言包
sudo apt install espeak-ng
#pluginlib
sudo apt-get install ros-$ROS_DISTRO-pluginlib -y
# 更新软件列表
sudo apt-get update
# 安装g++
sudo apt-get install g++
# 安装gcc
sudo apt-get install gcc
# 安装make
sudo apt-get install make
3. 核心概念
3.1 工作空间
什么是工作空间
工作空间是 ROS 系统中用于存放项目开发相关文件的文件夹,是整个开发过程中所有代码、参数、脚本等资料的“大本营”。每个工作空间包含开发中产生的所有必要文件。
- 本质: 工作空间就是一个管理项目文件的文件夹。
- 功能: 用于组织机器人开发的代码、参数配置、日志等。
- 自定义: 工作空间的名称可以自定义,且数量不唯一。
示例:
- 工作空间1:
dev_ws_a- 用于 A 机器人的开发。 - 工作空间2:
dev_ws_b- 用于 B 机器人的某些功能开发。 - 工作空间3:
dev_ws_b2- 用于 B 机器人的其他功能开发。
工作空间的典型结构

一个典型的工作空间结构如下:
dev_ws/
├── src # 代码空间,存放功能包源码
├── build # 编译空间,保存编译过程中的缓存信息和中间文件
├── install # 安装空间,存放编译后生成的可执行文件与脚本
├── log # 日志空间,存放编译和运行时的日志信息
使用说明
- 开发和操作主要集中在
src目录。 - 编译成功后,运行的是
install目录中的可执行文件。 build和log用于编译过程和日志记录,日常使用较少。
创建工作空间
mkdir -p ~/dev_ws/src
cd ~/dev_ws/src
git clone https://gitee.com/guyuehome/ros2_21_tutorials.git
自动安装依赖
我们从社区中下载的各种代码,多少都会有一些依赖,我们可以手动一个一个安装,也可以使用rosdep工具自动安装:
sudo apt install -y python3-pip
sudo pip3 install rosdepc
sudo rosdepc init
rosdepc update
cd ..
rosdepc install -i --from-path src --rosdistro humble -y
编译工作空间
依赖安装完成后,就可以使用如下命令编译工作空间啦,如果有缺少的依赖,或者代码有错误,编译过程中会有报错,否则编译过程应该不会出现任何错误:
sudo apt install python3-colcon-ros
cd ~/dev_ws/
colcon build
编译成功后,就可以在工作空间中看到自动生产的build、log、install文件夹了。
设置环境变量
编译成功后,为了让系统能够找到我们的功能包和可执行文件,还需要设置环境变量:
source install/local_setup.sh # 仅在当前终端生效
echo " source ~/dev_ws/install/local_setup.sh" >> ~/.bashrc # 所有终端均生效
3.2 功能包
功能包是什么
功能包是 ROS 中的核心机制,用于组织机器人不同功能的代码,方便管理与复用。
功能包的作用
- 模块化管理: 将机器人不同功能(如移动控制、视觉感知、自主导航)的源码分开存放,降低耦合性。
- 提高复用性: 功能包可以单独分享,只需说明如何使用,其他开发者即可快速集成。
- 便于维护: 不同功能的代码分散存储,便于维护和调试。
功能包的意义
举个例子:
- 如果把所有代码混合存放(如将红豆、绿豆、黄豆混在一起),很难在需要时快速提取某一功能代码。
- 而将代码划分为不同的功能包(如分别存放红豆、绿豆、黄豆),在需要时可以快速找到并提取对应功能代码。
创建功能包
在 ROS2 中,可以通过以下命令创建功能包:
ros2 pkg create --build-type <build-type> <package_name>
参数说明
-
pkg: 表示功能包相关的功能。
-
create: 表示创建功能包。
-
build-type : 指定功能包的构建类型:
ament_cmake: 用于 C++ 或 C。ament_python: 用于 Python。
-
package_name: 新建功能包的名称。
示例
在终端中分别创建 C++ 和 Python 版本的功能包:
cd ~/dev_ws/src
ros2 pkg create --build-type ament_cmake learning_pkg_c # C++
ros2 pkg create --build-type ament_python learning_pkg_python # Python
#编译
cd ~/dev_ws
colcon build # 编译工作空间中的所有功能包
source install/local_setup.bash # 配置环境变量
功能包的结构
功能包不是普通的文件夹,判断某文件夹是否为功能包的关键在于其包含的文件结构。以下是功能包的基本结构及特点:
C++ 功能包
C++ 功能包中,必然包含以下两个文件:
package.xml: 功能包的描述文件,主要内容包括:- 功能包的名称、版本号、描述信息。
- 版权声明。
- 各种依赖的声明。
CMakeLists.txt: 功能包的编译规则文件,使用 CMake 语法,指定如何编译 C++ 源代码。
Python 功能包
Python 功能包无需编译,其主要文件为:
-
package.xml: 功能包的描述文件,内容与 C++ 功能包类似。 -
- 功能包的安装和入口配置文件,主要内容包括:
setup.py- 版权声明。
entry_points配置,指定程序入口。
scripts:存储脚本文件,例如python源码或.sh脚本src: 存储C++源文件include:存储.h头文件launch:存储启动文件,可一次性运行多个节点config:存储配置信息
3.3 节点
节点是什么
机器人是各种功能的综合体,每一项功能就像机器人的一个工作细胞,众多细胞通过一些机制连接到一起,成为了一个机器人整体。在 ROS 中,这些“细胞”被称为 节点。
- 职责: 节点的职责是执行某些具体的任务。从计算机操作系统的角度来看,节点可以看作进程。
- 语言支持: 每个节点是一个独立的可执行文件,可以由 C++、Python、Java、Ruby 等多种语言编写。
- 分布式系统: 节点可以分布在不同的硬件设备上,例如计算机 A、计算机 B,甚至云端。
- 命名: 每个节点都有唯一的名称,用于查询或访问其状态。
类比
节点可以比喻为一个个工人,各自完成不同的任务。有的工人在一线厂房工作,有的在后勤保障,虽然互相不认识,但通过合作推动整个“机器人工厂”的运行。
实现流程
- 编程接口初始化。
- 创建节点并初始化。
- 实现节点功能。
- 销毁节点并关闭接口。
案例一:Hello World 节点(面向过程)
循环打印“Hello World”字符串:
$ ros2 run learning_node node_helloworld
代码解析
~/dev_ws/src/ros2_21_tutorials/learning_node/node_helloworld.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
import time
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = Node("node_helloworld") # 创建ROS2节点对象并进行初始化
while rclpy.ok(): # ROS2系统是否正常运行
node.get_logger().info("Hello World") # ROS2日志输出
time.sleep(0.5) # 休眠控制循环时间
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
入口配置
打开功能包的 ~/dev_ws/src/ros2_21_tutorials/learning_node/setup.py 文件,添加入口点:
entry_points={
'console_scripts': [
'node_helloworld = learning_node.node_helloworld:main',
],
}
案例二:Hello World 节点(面向对象)
循环打印“Hello World”字符串:
ros2 run learning_node node_helloworld_class
代码解析
~/dev_ws/src/ros2_21_tutorials/learning_node/node_helloworld_class.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
import time
#创建一个HelloWorld节点, 初始化时输出“hello world”日志
class HelloWorldNode(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
while rclpy.ok(): # ROS2系统是否正常运行
self.get_logger().info("Hello World") # ROS2日志输出
time.sleep(0.5) # 休眠控制循环时间
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = HelloWorldNode("node_helloworld_class") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
入口配置
打开功能包的 ~/dev_ws/src/ros2_21_tutorials/learning_node/setup.py 文件,添加入口点:
entry_points={
'console_scripts': [
'node_helloworld = learning_node.node_helloworld:main',
'node_helloworld_class = learning_node.node_helloworld_class:main',
],
}
节点并不是孤立的,他们之间会有很多种机制保持联系
话题、服务、动作对比
| 机制 | 描述 | 适用场景 |
|---|---|---|
| 话题 | 节点通过发布和订阅话题进行异步通信,适用于数据流较大的场景。 | 传感器数据、机器人状态 |
| 服务 | 节点通过请求和响应进行同步通信,适用于需要即时反馈的场景。 | 查询节点状态、快速计算、参数查询 |
| 动作 | 节点通过发送目标和接收反馈进行长时间任务的通信,适用于复杂任务控制。 | 机器人导航,机械臂操作 |


3.4 话题
话题是什么?
节点实现了机器人的各种功能,但这些功能之间并不是独立的,往往需要数据交换。在 ROS 中,节点之间传递数据的桥梁被称为 话题。
- 单向传输: 话题的通信模型是单向的,从一个节点到另一个节点。
- 异步通信: 发布者和订阅者无需同时在线,数据发布和接收可以是异步的。

以两个节点为例:
- 节点 A: 驱动相机,获取图像数据。
- 节点 B: 视频监控,显示节点 A 发布的图像数据。
这种从节点 A 到节点 B 的图像数据传输方式,称为 话题通信。话题是节点间实现单向数据传输的桥梁。
话题使用**.msg文件**定义
发布/订阅模型
话题基于 DDS(Data Distribution Service) 实现了发布/订阅模型:
- 发布者: 发送数据的节点。
- 订阅者: 接收数据的节点。
- 话题名称: 每个话题都有唯一的名称,用于区分不同数据流。
- 数据类型: 传输的数据有固定的数据格式,如
std_msgs/String、sensor_msgs/Image。
类比
- 一个微信公众号的名字相当于话题名称。
- 发布者是公众号小编,编辑和发布文章(消息)。
- 订阅者订阅公众号,接收文章。
话题通信特点
- 多对多通信:
- 一个话题可以有多个发布者和多个订阅者。
- 例如,多个摇杆节点可以控制一个机器人,也可以同时控制多个机器人。
- 异步通信:
- 发布者不知道订阅者何时接收消息,消息发送后发布者可以继续运行。
- 更适合周期性数据传输,如传感器数据或运动控制指令。
- 消息接口:
- 数据需要有统一的格式描述,称为 消息(Message),类似编程语言中的数据结构。
- 消息可以使用
.msg文件自定义,也可以使用 ROS 标准消息类型。
如何实现话题通信?
| 发布者 | 订阅者 |
|---|---|
| 1. 编程接口初始化 | 1. 编程接口初始化 |
| 2. 创建节点并初始化 | 2. 创建节点并初始化 |
| 3. 创建发布者对象 | 3. 创建订阅者对象 |
| 4. 创建并填充话题消息 | 4. 回调函数处理话题数据 |
| 5. 发布话题消息 | 5. 销毁节点并关闭接口 |
| 6. 销毁节点并关闭接口 |
案例一:Hello World 话题通信
目标
创建一个发布者节点和一个订阅者节点,通过话题 “chatter” 实现字符串 “Hello World” 的通信。消息类型为 std_msgs/String。

运行
- 启动发布者节点:
$ ros2 run learning_topic topic_helloworld_pub - 启动订阅者节点
$ ros2 run learning_topic topic_helloworld_sub
代码解析
- 发布者
learning_topic/topic_helloworld_pub.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from std_msgs.msg import String # 字符串消息类型
#创建一个发布者节点
class PublisherNode(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.pub = self.create_publisher(String, "chatter", 10) # 创建发布者对象(消息类型、话题名、队列长度)
self.timer = self.create_timer(0.5, self.timer_callback) # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
def timer_callback(self): # 创建定时器周期执行的回调函数
msg = String() # 创建一个String类型的消息对象
msg.data = 'Hello World' # 填充消息对象中的消息数据
self.pub.publish(msg) # 发布话题消息
self.get_logger().info('Publishing: "%s"' % msg.data) # 输出日志信息,提示已经完成话题发布
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = PublisherNode("topic_helloworld_pub") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
setup.py:
entry_points={
'console_scripts': [
'topic_helloworld_pub = learning_topic.topic_helloworld_pub:main',
],
},
- 订阅者
learning_topic/topic_helloworld_sub.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from std_msgs.msg import String # ROS2标准定义的String消息
#创建一个订阅者节点
class SubscriberNode(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.sub = self.create_subscription(\
String, "chatter", self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
def listener_callback(self, msg): # 创建回调函数,执行收到话题消息后对数据的处理
self.get_logger().info('I heard: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = SubscriberNode("topic_helloworld_sub") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
setup.py:
entry_points={
'console_scripts': [
'topic_helloworld_pub = learning_topic.topic_helloworld_pub:main',
'topic_helloworld_sub = learning_topic.topic_helloworld_sub:main',
],
},
3.5 服务
服务是什么
服务是一种实现 节点之间同步通信 的机制,通过“请求 - 应答”的方式进行数据交换。
与话题的单向异步通信不同,服务基于 客户端/服务器(CS)模型。
服务使用的是**.srv文件**定义

特点
- 同步通信: 客户端等待服务器返回结果。
- 一对多通信: 一个服务器可以服务多个客户端。
- 服务接口:
- 数据分为 请求数据 和 反馈数据。
- 定义文件使用
.srv,类似话题的.msg文件。
服务的实现方法
| 客户端 | 服务端 |
|---|---|
| 编程接口初始化 | 编程接口初始化 |
| 创建节点并初始化 | 创建节点并初始化 |
| 创建客户端对象 | 创建服务器端对象 |
| 创建并发送请求数据 | 通过回调函数处理服务 |
| 等待服务器端应答数据 | 向客户端反馈应答结果 |
| 销毁节点并关闭接口 | 销毁节点并关闭接口 |
案例一:加法求解器
目标
实现一个服务,用于加法计算:
- 客户端: 发送两个加数的请求。
- 服务器: 接收请求并返回加法结果。
运行效果
-
启动服务端节点:
ros2 run learning_service service_adder_server -
启动客户端节点:
$ ros2 run learning_service service_adder_client 2 3

客户端代码
learning_service/service_adder_client.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#说明: ROS2服务示例-发送两个加数,请求加法器计算
import sys
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口
class adderClient(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.client = self.create_client(AddTwoInts, 'add_two_ints') # 创建服务客户端对象(服务接口类型,服务名)
while not self.client.wait_for_service(timeout_sec=1.0): # 循环等待服务器端成功启动
self.get_logger().info('service not available, waiting again...')
self.request = AddTwoInts.Request() # 创建服务请求的数据对象
def send_request(self): # 创建一个发送服务请求的函数
self.request.a = int(sys.argv[1])
self.request.b = int(sys.argv[2])
self.future = self.client.call_async(self.request) # 异步方式发送服务请求
def main(args=None):
rclpy.init(args=args) # ROS2 Python接口初始化
node = adderClient("service_adder_client") # 创建ROS2节点对象并进行初始化
node.send_request() # 发送服务请求
while rclpy.ok(): # ROS2系统正常运行
rclpy.spin_once(node) # 循环执行一次节点
if node.future.done(): # 数据是否处理完成
try:
response = node.future.result() # 接收服务器端的反馈数据
except Exception as e:
node.get_logger().info(
'Service call failed %r' % (e,))
else:
node.get_logger().info( # 将收到的反馈信息打印输出
'Result of add_two_ints: for %d + %d = %d' %
(node.request.a, node.request.b, response.sum))
break
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
setup.py:
entry_points={
'console_scripts': [
'service_adder_client = learning_servicservice_adder_client:main',
],
}
服务端代码
learning_service/service_adder_server.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供加法器的服务器处理功能
"""
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口
class adderServer(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.adder_callback) # 创建服务器对象(接口类型、服务名、服务器回调函数)
def adder_callback(self, request, response): # 创建回调函数,执行收到请求后对数据的处理
response.sum = request.a + request.b # 完成加法求和计算,将结果放到反馈的数据中
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) # 输出日志信息,提示已经完成加法求和计算
return response # 反馈应答信息
def main(args=None): # ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = adderServer("service_adder_server") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
setup.py:
entry_points={
'console_scripts': [
'service_adder_client = learning_servicservice_adder_client:main',
'service_adder_server = learning_servicservice_adder_server:main',
],
},
3.6 通信接口
3.7 动作
动作是什么
动作是一种 应用层通信机制,适合对机器人 完整行为流程 进行管理,比如旋转到某个角度、移动到某个目标点等。动作通信支持 进度反馈 和 终止控制,让用户可以更好地管理行为执行。
动作使用的也是客户端和服务器模型。客户端发送动作的目标,服务器端执行动作过程,控制机器人达到运动的目标,同时周期反馈动作执行过程中的状态。
动作的底层是基于话题和服务来实现的。

特点
- 同步通信: 支持实时反馈进度和动作完成状态。
- 一对多通信: 一个服务器可以服务多个客户端。
- 底层实现: 动作是基于 服务和话题 的组合通信机制:
- 服务: 用于发送目标和接收结果。
- 话题: 用于发布动作的周期反馈。
案例一:小海龟动作
通过动作接口控制小海龟的旋转行为:
运行效果
-
启动小海龟仿真器:
ros2 run turtlesim turtlesim_node -
发送动作目标
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback
案例二:机器人画圆
动作虽然是基于话题和服务实现的,但在实际使用中,并不会直接使用话题和服务的编程方法,而是有一套针对动作特性封装好的编程接口
-
启动
ros2 run learning_action action_move_server ros2 run learning_action action_move_client -
实现原理
动作接口定义
learning_interface/action/MoveCircle.action:bool enable # 动作目标,true 表示开始执行动作 bool finish # 动作结果,true 表示完成动作 int32 state # 动作反馈,当前旋转角度定义完成后,在
CMakeLists.txt中添加以下配置,让编译器生成接口相关代码:find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "action/MoveCircle.action" )服务端代码解析
learning_action/action_move_server.py:#!/usr/bin/env python3 # -*- coding: utf-8 -*- #说明: ROS2动作示例-负责执行圆周运动动作的服务端 import time import rclpy # ROS2 Python接口库 from rclpy.node import Node # ROS2 节点类 from rclpy.action import ActionServer # ROS2 动作服务器类 from learning_interface.action import MoveCircle # 自定义的圆周运动接口 class MoveCircleActionServer(Node): def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self._action_server = ActionServer( # 创建动作服务器(接口类型、动作名、回调函数) self, MoveCircle, 'move_circle', self.execute_callback) def execute_callback(self, goal_handle): # 执行收到动作目标之后的处理函数 self.get_logger().info('Moving circle...') feedback_msg = MoveCircle.Feedback() # 创建一个动作反馈信息的消息 for i in range(0, 360, 30): # 从0到360度,执行圆周运动,并周期反馈信息 feedback_msg.state = i # 创建反馈信息,表示当前执行到的角度 self.get_logger().info('Publishing feedback: %d' % feedback_msg.state) goal_handle.publish_feedback(feedback_msg) # 发布反馈信息 time.sleep(0.5) goal_handle.succeed() # 动作执行成功 result = MoveCircle.Result() # 创建结果消息 result.finish = True return result # 反馈最终动作执行的结果 def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = MoveCircleActionServer("action_move_server") # 创建ROS2节点对象并进行初始化 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口setup.py:entry_points={ 'console_scripts': [ 'action_move_server = learning_action.action_move_server:main', ], },客户端代码解析
learning_action/action_move_client.py:#!/usr/bin/env python3 # -*- coding: utf-8 -*- #说明: ROS2动作示例-请求执行圆周运动动作的客户端 import rclpy # ROS2 Python接口库 from rclpy.node import Node # ROS2 节点类 from rclpy.action import ActionClient # ROS2 动作客户端类 from learning_interface.action import MoveCircle # 自定义的圆周运动接口 class MoveCircleActionClient(Node): def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self._action_client = ActionClient( # 创建动作客户端(接口类型、动作名) self, MoveCircle, 'move_circle') def send_goal(self, enable): # 创建一个发送动作目标的函数 goal_msg = MoveCircle.Goal() # 创建一个动作目标的消息 goal_msg.enable = enable # 设置动作目标为使能,希望机器人开始运动 self._action_client.wait_for_server() # 等待动作的服务器端启动 self._send_goal_future = self._action_client.send_goal_async( # 异步方式发送动作的目标 goal_msg, # 动作目标 feedback_callback=self.feedback_callback) # 处理周期反馈消息的回调函数 self._send_goal_future.add_done_callback(self.goal_response_callback) # 设置一个服务器收到目标之后反馈时的回调函数 def goal_response_callback(self, future): # 创建一个服务器收到目标之后反馈时的回调函数 goal_handle = future.result() # 接收动作的结果 if not goal_handle.accepted: # 如果动作被拒绝执行 self.get_logger().info('Goal rejected :(') return self.get_logger().info('Goal accepted :)') # 动作被顺利执行 self._get_result_future = goal_handle.get_result_async() # 异步获取动作最终执行的结果反馈 self._get_result_future.add_done_callback(self.get_result_callback) # 设置一个收到最终结果的回调函数 def get_result_callback(self, future): # 创建一个收到最终结果的回调函数 result = future.result().result # 读取动作执行的结果 self.get_logger().info('Result: {%d}' % result.finish) # 日志输出执行结果 def feedback_callback(self, feedback_msg): # 创建处理周期反馈消息的回调函数 feedback = feedback_msg.feedback # 读取反馈的数据 self.get_logger().info('Received feedback: {%d}' % feedback.state) def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = MoveCircleActionClient("action_move_client") # 创建ROS2节点对象并进行初始化 node.send_goal(True) # 发送动作目标 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口setup.py:entry_points={ 'console_scripts': [ 'action_move_client = learning_action.action_move_client:main', 'action_move_server = learning_action.action_move_server:main', ], },
3.8 参数
什么是参数?
参数是一种常用的数据传输方式,类似C++编程中的全局变量,可以便于在多个程序中共享某些数据。
参数是ROS机器人系统中的全局字典,可以运行多个节点中共享数据。
参数的特性
- 全局字典:参数由名称(键)和数值(值)组成,节点间可以通过访问参数名称获取对应的值。
- 动态监控**:当参数的值被修改,其他节点可以实时检测到并使用新值。
- 参数文件:参数可以保存到 YAML 文件中,也可以从 YAML 文件加载。
案例一:小海龟的参数
-
启动仿真器和键盘控制节点:
ros2 run turtlesim turtlesim_node ros2 run turtlesim turtle_teleop_key -
查看参数列表:
ros2 param list -
参数查询与修改:
ros2 param describe turtlesim background_b # 查看某个参数的描述信息 ros2 param get turtlesim background_b # 查询某个参数的值 ros2 param set turtlesim background_b 10 # 修改某个参数的值 -
参数文件保存与加载:
#保存当前节点的参数到文件: ros2 param dump turtlesim >> turtlesim.yaml #从文件加载参数: ros2 param load turtlesim turtlesim.yaml
3.9 分布式通信
什么是分布式通信?
在 ROS 系统中,机器人功能由多个节点组成,这些节点可以分布在不同的计算平台上,形成分布式系统。通过这种方式,可以将资源密集型任务分配到性能更强的计算机中,从而提高系统的整体效率。
示例场景
以一个双平台的机器人系统为例:
- 树莓派:用于传感器驱动和电机控制。
- 笔记本电脑:用于视觉处理和高级应用功能。
分布式网络分组
为了限制通信范围,可以使用 ROS_DOMAIN_ID 机制,将不同计算机分配到不同的分组中。
配置方法
在电脑和树莓派端的.bashrc 中加入这样一句配置:
export ROS_DOMAIN_ID=<your_domain_id>
只有相同的 DOMAIN_ID才可以通信。
3.10 DDS
什么是DDS?
DDS(Data Distribution Service,数据分发服务)核心功能是实现以数据为中心的通信机制。ROS2 系统中的话题、服务、动作等通信都基于 DDS 实现。
- 数据为中心:以数据分发为核心设计。
- 服务质量(QoS)策略:支持实时、高效、灵活的通信需求。
- 分布式实时通信:满足复杂系统中高频通信的需求。
DDS 提供了一种高效的通信方式,与传统的通信模型相比具有显著优势:
| 模型 | 描述 |
|---|---|
| 点对点模型 | 通信双方必须建立连接,节点增多时连接数激增,且客户端需知道服务器地址。 |
| Broker 模型 | 中央 Broker 处理所有请求,但性能受限,单点故障可能导致整个系统瘫痪(ROS1 使用该模型)。 |
| 广播模型 | 所有节点均可接收广播消息,但消息冗余多,节点需处理所有消息。 |
| DDS 模型 | 提供 DataBus 数据总线,仅订阅感兴趣的数据,支持高效并行,通信双方无需建立直接连接。 |
DDS 的核心功能
-
Domain 分组通信
通过DOMAIN_ID将节点分组,仅同组节点能互相通信,避免无关数据干扰。 -
服务质量(QoS)策略
QoS 是 DDS 的核心功能,是一种网络传输策略,用于指定通信的传输质量要求。常见策略包括:
- DEADLINE:通信数据需在截止时间内完成传输。
- HISTORY:定义历史数据的缓存大小。
- RELIABILITY:传输模式,包括
BEST_EFFORT(尽力传输,可能丢失数据)RELIABLE(保证数据完整性)。 - DURABILITY:确保晚加入节点可接收到历史数据。
- 应用场景
- 无人机遥控:命令采用
RELIABLE模式,保证每个指令送达;视频流采用BEST_EFFORT模式,确保流畅。 - 数据加密:DDS 支持通信数据加密,提升安全性。
案例一:命令行配置 DDS
-
启动发布者节点,使用
BEST_EFFORT模式:ros2 topic pub /chatter std_msgs/msg/Int32 "data: 42" --qos-reliability best_effort -
启动订阅者节点,使用
RELIABLE模式(无法接收数据):ros2 topic echo /chatter --qos-reliability reliable -
修改订阅者为
BEST_EFFORT模式(成功接收数据):ros2 topic echo /chatter --qos-reliability best_effort
4. 常用工具
4.1 Launch
Launch 简介
ROS系统中多节点启动与配置的一种脚本
-
启动
ros2 launch learning_launch simple.launch.py -
原理分析
示例1
learning_launch/simple.launch.py:from launch import LaunchDescription # launch文件的描述类 from launch_ros.actions import Node # 节点启动的描述类 def generate_launch_description(): # 自动生成launch文件的函数 return LaunchDescription([ # 返回launch文件的描述信息 Node( # 配置一个节点的启动 package='learning_topic', # 节点所在的功能包 executable='topic_helloworld_pub', # 节点的可执行文件 ), Node( # 配置一个节点的启动 package='learning_topic', # 节点所在的功能包 executable='topic_helloworld_sub', # 节点的可执行文件名 ), ])示例2
learning_launch/rviz.launch.py:import os from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法 from launch import LaunchDescription # launch文件的描述类 from launch_ros.actions import Node # 节点启动的描述类 def generate_launch_description(): # 自动生成launch文件的函 数 rviz_config = os.path.join( # 找到配置文件的完整路径 get_package_share_directory('learning_launch'), 'rviz', 'turtle_rviz.rviz' ) return LaunchDescription([ # 返回launch文件的描述信息 Node( # 配置一个节点的启动 package='rviz2', # 节点所在的功能包 executable='rviz2', # 节点的可执行文件名 name='rviz2', # 对节点重新命名 arguments=['-d', rviz_config] # 加载命令行参数 ) ])
资源重映射
-
启动
ros2 launch learning_launch remapping.launch.py ros2 topic pub --rate 1 /turtlesim1/turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}" #转圈通过话题重映射实现同步运动

-
原理分析
learning_launch/remapping.launch.py:
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
namespace='turtlesim1', # 节点所在的命名空间
executable='turtlesim_node', # 节点的可执行文件名
name='sim' # 对节点重新命名
),
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
namespace='turtlesim2', # 节点所在的命名空间
executable='turtlesim_node', # 节点的可执行文件名
name='sim' # 对节点重新命名
),
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
executable='mimic', # 节点的可执行文件名
name='mimic', # 对节点重新命名
remappings=[ # 资源重映射列表
('/input/pose', '/turtlesim1/turtle1/pose'), # 将/input/pose话题名为/turtlesim1/turtle1/pose
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), # 将/output/cmd_vel话题名修改为/turtlesim2/turtle1/cmd_vel
]
)
])
在 setup.py 中添加如下配置以支持 Launch 文件::
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))),
(os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.*'))),
(os.path.join('share', package_name, 'rviz'), glob(os.path.join('rviz', '*.*'))),
],
4.2 TF
TF 是 ROS 中的重要工具,用于管理机器人系统中的各种坐标系关系,支持静态和动态坐标变换以及坐标系查询。
1. 机器人中的常见坐标系
-
机械臂机器人:
- 基坐标系:Base Frame
- 世界坐标系:World Frame
- 工具坐标系:Tool Frame
- 工件坐标系:Object Frame

-
移动机器人:
- 里程计坐标系:Odom Frame
- 地图坐标系:Map Frame
- 激光雷达坐标系:Laser Frame
- 基坐标系:Base Link

这些坐标系之间的关系可用 4x4 的齐次变换矩阵描述,包含平移和旋转。
例程一 小海龟跟随例程
ros2 launch turtle_tf2_py turtle_tf2_demo.launch.py
ros2 run turtlesim turtle_teleop_key
启动了四个节点,分别是:
- 小海龟仿真器
- 海龟1的坐标系广播
- 海龟2的坐标系广播
- 海龟跟随控制
查看TF树
默认在当前终端路径下生成了一个frames.pdf文件,打开之后,就可以看到系统中各个坐标系的关系了
ros2 run tf2_tools view_frames

查询坐标变换信息
ros2 run tf2_ros tf2_echo turtle2 turtle1
循环打印坐标系的变换数值了,由平移和旋转两个部分组成,还有旋转矩阵
At time 1736842433.4032845
- Translation: [0.000, 0.000, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.985, -0.173]
- Rotation: in RPY (radian) [0.000, -0.000, -2.793]
- Rotation: in RPY (degree) [0.000, -0.000, -160.030]
- Matrix:
-0.940 0.342 0.000 0.000
-0.342 -0.940 0.000 0.000
0.000 0.000 1.000 0.000
0.000 0.000 0.000 1.000
坐标系可视化
ros2 run rviz2 rviz2 -d $(ros2 pkg prefix --share turtle_tf2_py)/rviz/turtle_rviz.rviz

2. 静态TF变换
运行
ros2 run learning_tf static_tf_broadcaster
ros2 run tf2_tools view_frames

代码解析
learning_tf/static_tf_broadcaster.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#说明: ROS2 TF示例-广播静态的坐标变换
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from geometry_msgs.msg import TransformStamped # 坐标变换消息
import tf_transformations # TF坐标变换库
from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster # TF静态坐标系广播器类
class StaticTFBroadcaster(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.tf_broadcaster = StaticTransformBroadcaster(self) # 创建一个TF广播器对象
static_transformStamped = TransformStamped() # 创建一个坐标变换的消息对象
static_transformStamped.header.stamp = self.get_clock().now().to_msg() # 设置坐标变换消息的时间戳
static_transformStamped.header.frame_id = 'world' # 设置一个坐标变换的源坐标系
static_transformStamped.child_frame_id = 'house' # 设置一个坐标变换的目标坐标系
static_transformStamped.transform.translation.x = 10.0 # 设置坐标变换中的X、Y、Z向的平移
static_transformStamped.transform.translation.y = 5.0
static_transformStamped.transform.translation.z = 0.0
quat = tf_transformations.quaternion_from_euler(0.0, 0.0, 0.0) # 将欧拉角转换为四元数(roll, pitch, yaw)
static_transformStamped.transform.rotation.x = quat[0] # 设置坐标变换中的X、Y、Z向的旋转(四元数)
static_transformStamped.transform.rotation.y = quat[1]
static_transformStamped.transform.rotation.z = quat[2]
static_transformStamped.transform.rotation.w = quat[3]
self.tf_broadcaster.sendTransform(static_transformStamped) # 广播静态坐标变换,广播后两个坐标系的位置关系保持不变
def main(args=None):
rclpy.init(args=args) # ROS2 Python接口初始化
node = StaticTFBroadcaster("static_tf_broadcaster") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown()
setup.py:
entry_points={
'console_scripts': [
'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
],
},
3. TF监听
查询两个坐标系之间的位置关系
运行
ros2 run learning_tf tf_listener
代码解析
learning_tf/tf_listener.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#说明: ROS2 TF示例-监听某两个坐标系之间的变换
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
import tf_transformations # TF坐标变换库
from tf2_ros import TransformException # TF左边变换的异常类
from tf2_ros.buffer import Buffer # 存储坐标变换信息的缓冲类
from tf2_ros.transform_listener import TransformListener # 监听坐标变换的监听器类
class TFListener(Node):
def __init__(self, name):
super().__init__(name) # ROS2节点父类初始化
self.declare_parameter('source_frame', 'world') # 创建一个源坐标系名的参数
self.source_frame = self.get_parameter( # 优先使用外部设置的参数值,否则用默认值
'source_frame').get_parameter_value().string_value
self.declare_parameter('target_frame', 'house') # 创建一个目标坐标系名的参数
self.target_frame = self.get_parameter( # 优先使用外部设置的参数值,否则用默认值
'target_frame').get_parameter_value().string_value
self.tf_buffer = Buffer() # 创建保存坐标变换信息的缓冲区
self.tf_listener = TransformListener(self.tf_buffer, self) # 创建坐标变换的监听器
self.timer = self.create_timer(1.0, self.on_timer) # 创建一个固定周期的定时器,处理坐标信息
def on_timer(self):
try:
now = rclpy.time.Time() # 获取ROS系统的当前时间
trans = self.tf_buffer.lookup_transform( # 监听当前时刻源坐标系到目标坐标系的坐标变换
self.target_frame,
self.source_frame,
now)
except TransformException as ex: # 如果坐标变换获取失败,进入异常报告
self.get_logger().info(
f'Could not transform {self.target_frame} to {self.source_frame}: {ex}')
return
pos = trans.transform.translation # 获取位置信息
quat = trans.transform.rotation # 获取姿态信息(四元数)
euler = tf_transformations.euler_from_quaternion([quat.x, quat.y, quat.z, quat.w])
self.get_logger().info('Get %s --> %s transform: [%f, %f, %f] [%f, %f, %f]'
% (self.source_frame, self.target_frame, pos.x, pos.y, pos.z, euler[0], euler[1], euler[2]))
def main(args=None):
rclpy.init(args=args) # ROS2 Python接口初始化
node = TFListener("tf_listener") # 创建ROS2节点对象并进行初始化
rclpy.spin(node) # 循环等待ROS2退出
node.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
setup.py:
entry_points={
'console_scripts': [
'static_tf_broadcaster = learning_tf.static_tf_broadcaster:main',
'tf_listener = learning_tf.tf_listener:main',
],
},
4.3 URDF
URDF(统一机器人描述格式)是 ROS 中用于描述机器人模型的标准方法。通过 URDF,我们可以定义机器人的外观、结构、传感器位置以及运动学等属性。
1. 机器人的基本组成
机器人通常由以下四个部分组成:
- 硬件结构:底盘、外壳、电机等。
- 驱动系统:电机驱动器、电源管理等。
- 传感系统:编码器、IMU、摄像头、激光雷达等。
- 控制系统:计算平台(如树莓派、PC)及其操作系统和软件。
2. URDF 的核心概念
2.1 URDF 文件结构
URDF 使用 XML 格式定义机器人模型。主要标签包括:
<robot>:表示整个机器人模型。<link>:描述机器人模型的刚体部分(如底盘、连杆)。<joint>:描述机器人连杆之间的连接及运动类型。
2.2 连杆(Link)
连杆是机器人模型的基本构件,用 <link> 标签定义。每个连杆包含以下属性:
- 名称:
<link name="link_name"> - 视觉属性(外观):
<visual>- 几何形状:
<geometry>(如<cylinder>、<sphere>) - 材质:
<material>和<color>
- 几何形状:
- 碰撞属性(计算时的简化模型):
<collision>
示例
以下是一个描述机械臂连杆的示例:
<link name="base_link">
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.16" radius="0.20"/>
</geometry>
<material name="yellow">
<color rgba="1 0.4 0 1"/>
</material>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<cylinder length="0.16" radius="0.20"/>
</geometry>
</collision>
</link>
2.3 关节(Joint)
关节用于连接连杆,并定义连杆之间的运动。URDF 支持以下六种关节类型:
- 连续关节(
continuous):可无限旋转(如车轮)。 - 旋转关节(
revolute):旋转范围有限。 - 滑动关节(
prismatic):沿某轴滑动。 - 固定关节(
fixed):无相对运动。 - 浮动关节(
floating):自由运动。 - 平面关节(
planar):平面内的运动。
示例
以下是一个描述旋转关节的示例:
<joint name="left_wheel_joint" type="continuous">
<origin xyz="0 0.19 -0.05" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="left_wheel_link"/>
<axis xyz="0 1 0"/>
</joint>
3. 创建机器人模型
功能包结构
learning_urdf/
├── urdf/ # 机器人模型文件
├── meshes/ # 三维模型文件
├── launch/ # 启动文件
├── rviz/ # RViz 配置文件
#可视化机器人模型
ros2 launch learning_urdf display.launch.py
#查看URDF模型结构
urdf_to_graphviz mbot_base.urdf # 在模型文件夹下运行

4.4 Gazebo
Gazebo是ROS系统中最为常用的三维物理仿真平台,支持动力学引擎,可以实现高质量的图形渲染,不仅可以模拟机器人及周边环境,还可以加入摩擦力、弹性系数等物理属性。
启动
ros2 launch gazebo_ros gazebo.launch.py
设计好的URDF模型此时还不能直接放到Gazebo中,需要我们做一些优化
1. XACRO机器人模型优化
同样也是对机器人URDF模型的创建,XACRO文件加入了更多编程化的实现方法,可以让模型创建更友好。
- 宏定义,一个小车有4个轮子,每个轮子都一样,我们就没必要创建4个一样的link,像函数定义一样,做一个可重复使用的模块就可以了。
- 文件包含,复杂机器人的模型文件可能会很长,为了切分不同的模块,比如底盘、传感器,我们还可以把不同模块的模型放置在不同的文件中,然后再用一个总体文件做包含调用。
- 可编程接口,比如在XACRO模型文件中,定义一些常量,描述机器人的尺寸,定义一些变量,在调用宏定义的时候传递数据,还可以在模型中做数据计算,甚至加入条件语句,比如你的机器人叫A,就有摄像头,如果叫B,就没有摄像头。



2. 机器人仿真模型配置
-
完善物理参数
确保每一个link都有惯性参数和碰撞属性。
-
添加Gazebo标签
可以在gazebo中渲染每一个link的颜色,因为URDF中的颜色系统和gazebo中的不同。
-
配置传动装置
给运动的joint配置传动装置,可以理解为仿真了一个电机。
-
添加控制器插件
添加一个gazebo的控制器插件,小车是差速控制的,那就添加差速控制器插件,这样在不同角度下两个电机的速度分配,就可以交给控制器插件来完成了。
3. 构建仿真环境
把模型加载到Gazebo中了,需要用到一个gazebo提供的功能节点spwan_entity。
learning_gazebo/launch/load_urdf_into_gazebo.launch.py:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
# Include the robot_state_publisher launch file, provided by our own package. Force sim time to be enabled
# !!! MAKE SURE YOU SET THE PACKAGE NAME CORRECTLY !!!
package_name='learning_gazebo' #<--- CHANGE ME
world_file_path = 'worlds/neighborhood.world'
pkg_path = os.path.join(get_package_share_directory(package_name))
world_path = os.path.join(pkg_path, world_file_path)
# Pose where we want to spawn the robot
spawn_x_val = '0.0'
spawn_y_val = '0.0'
spawn_z_val = '0.0'
spawn_yaw_val = '0.0'
mbot = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory(package_name),'launch','mbot.launch.py'
)]), launch_arguments={'use_sim_time': 'true', 'world':world_path}.items()
)
# Include the Gazebo launch file, provided by the gazebo_ros package
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('gazebo_ros'), 'launch', 'gazebo.launch.py')]),
)
# Run the spawner node from the gazebo_ros package. The entity name doesn't really matter if you only have a single robot.
spawn_entity = Node(package='gazebo_ros', executable='spawn_entity.py',
arguments=['-topic', 'robot_description',
'-entity', 'mbot',
'-x', spawn_x_val,
'-y', spawn_y_val,
'-z', spawn_z_val,
'-Y', spawn_yaw_val],
output='screen')
# Launch them all!
return LaunchDescription([
mbot,
gazebo,
spawn_entity,
])
5. 机器人运动仿真
一个简单的模型仿真
#启动仿真环境
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
#启动键盘控制
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=/burger/cmd_vel
一个复杂的模型仿真
ros2 launch learning_gazebo load_urdf_into_gazebo.launch.py #启动仿真环境
ros2 run teleop_twist_keyboard teleop_twist_keyboard #键盘控制
6. Ignition 仿真
ros2 launch ros_ign_gazebo_demos rgbd_camera_bridge.launch.py
官网:www.ignitionrobotics.org/
7. 机器人模型
位置
learning_gazebo/urdf/mbot_gazebo.xacro
learning_gazebo/urdf/mbot_base_gazebo.xacro
4.5 Rviz
Rviz 是 ROS 中常用的三维可视化工具,用于显示机器人模型、传感器数据、运动轨迹等信息。
1. 启动 Rviz
ros2 run rviz2 rviz2
2. 彩色相机仿真与可视化
插件名称:libgazebo_ros_camera.so
learning_gazebo/urdf/sensers/camera_gazebo.xacro:
<gazebo reference="${prefix}_link"> <!-- 设置插件依附的链接 -->
<sensor type="camera" name="camera_node"> <!-- 定义传感器类型为相机 -->
<update_rate>30.0</update_rate> <!-- 设置相机更新频率为 30Hz -->
<camera> <!-- 相机配置参数 -->
<horizontal_fov>1.3962634</horizontal_fov> <!-- 相机水平视场角 -->
<image> <!-- 图像输出设置 -->
<width>1280</width> <!-- 图像宽度 -->
<height>720</height> <!-- 图像高度 -->
<format>R8G8B8</format> <!-- 图像颜色格式 -->
</image>
<clip> <!-- 图像可见范围 -->
<near>0.02</near> <!-- 最近可见距离 -->
<far>300</far> <!-- 最远可见距离 -->
</clip>
</camera>
<plugin name="gazebo_camera" filename="libgazebo_ros_camera.so"> <!-- 加载 Gazebo 相机插件 -->
<remapping>~/image_raw:=image_raw</remapping> <!-- 重映射图像输出话题 -->
</plugin>
</sensor>
</gazebo>
主要配置项如下:
- 标签:描述传感器
type:传感器类型,camera
name:摄像头命名,自由设置
- 标签:描述摄像头参数
分辨率,编码格式,图像范围,噪音参数等
- 标签:加载摄像头仿真插件
运行
ros2 launch learning_gazebo load_mbot_camera_into_gazebo.launch.py
ros2 run teleop_twist_keyboard teleop_twist_keyboard #键盘控制
ros2 run rviz2 rviz2
在左侧Displays窗口中点击“Add”,找到Image显示项,设置topic为/camera/image_raw,即可看到相机图像。


3. 三维相机仿真与可视化
插件名称:libgazebo_ros_ray_sensor.so
learning_gazebo/urdf/sensers/kinect_gazebo.xacro:
```xml
<gazebo reference="${prefix}_link"> <!-- 设置插件依附的链接 -->
<sensor type="depth" name="${prefix}"> <!-- 定义传感器类型为深度相机 -->
<always_on>true</always_on> <!-- 设置传感器始终开启 -->
<update_rate>15.0</update_rate> <!-- 设置更新频率为 15Hz -->
<pose>0 0 0 0 0 0</pose> <!-- 设置传感器的位姿 -->
<camera name="kinect"> <!-- 相机配置参数 -->
<horizontal_fov>${60.0*M_PI/180.0}</horizontal_fov> <!-- 相机水平视场角 -->
<image> <!-- 图像输出设置 -->
<format>R8G8B8</format> <!-- 图像颜色格式 -->
<width>640</width> <!-- 图像宽度 -->
<height>480</height> <!-- 图像高度 -->
</image>
<clip> <!-- 图像可见范围 -->
<near>0.05</near> <!-- 最近可见距离 -->
<far>8.0</far> <!-- 最远可见距离 -->
</clip>
</camera>
<plugin name="${prefix}_controller" filename="libgazebo_ros_camera.so"> <!-- 加载 Gazebo 相机插件 -->
<ros> <!-- ROS 配置 -->
<!-- <namespace>${prefix}</namespace> -->
<remapping>${prefix}/image_raw:=rgb/image_raw</remapping> <!-- 重映射图像输出话题 -->
<remapping>${prefix}/image_depth:=depth/image_raw</remapping> <!-- 重映射深度图像输出话题 -->
<remapping>${prefix}/camera_info:=rgb/camera_info</remapping> <!-- 重映射相机信息话题 -->
<remapping>${prefix}/camera_info_depth:=depth/camera_info</remapping> <!-- 重映射深度相机信息话题 -->
<remapping>${prefix}/points:=depth/points</remapping> <!-- 重映射点云话题 -->
</ros>
<camera_name>${prefix}</camera_name> <!-- 设置相机名称 -->
<frame_name>${prefix}_frame_optical</frame_name> <!-- 设置相机坐标系名称 -->
<hack_baseline>0.07</hack_baseline> <!-- 设置相机基线距离 -->
<min_depth>0.001</min_depth> <!-- 设置最小深度值 -->
<max_depth>300.0</max_depth> <!-- 设置最大深度值 -->
</plugin>
</sensor>
</gazebo>
运行
ros2 launch learning_gazebo load_mbot_rgbd_into_gazebo.launch.py
ros2 run teleop_twist_keyboard teleop_twist_keyboard #键盘控制
ros2 run rviz2 rviz2
点击Add,添加PointCloud2,设置订阅的点云话题,还要配置Rviz的参考系是odom,就可以看到点云数据

4. 激光雷达仿真与可视化
插件名称:libgazebo_ros_ray_sensor.so
learning_gazebo/urdf/sensers/lidar_gazebo.xacro:
<gazebo reference="${prefix}_link"> <!-- 设置插件依附的链接 -->
<sensor type="ray" name="rplidar"> <!-- 定义传感器类型为激光雷达 -->
<update_rate>20</update_rate> <!-- 设置更新频率为 20Hz -->
<ray> <!-- 激光雷达配置参数 -->
<scan>
<horizontal> <!-- 水平扫描设置 -->
<samples>360</samples> <!-- 水平扫描的采样点数 -->
<resolution>1</resolution> <!-- 水平分辨率 -->
<min_angle>-3</min_angle> <!-- 最小扫描角度 -->
<max_angle>3</max_angle> <!-- 最大扫描角度 -->
</horizontal>
</scan>
<range> <!-- 距离范围设置 -->
<min>0.10</min> <!-- 最小测距 -->
<max>30.0</max> <!-- 最大测距 -->
<resolution>0.01</resolution> <!-- 距离分辨率 -->
</range>
<noise>
<type>gaussian</type> <!-- 噪声类型 -->
<mean>0.0</mean> <!-- 均值 -->
<stddev>0.01</stddev> <!-- 标准差 -->
</noise>
</ray>
<plugin name="gazebo_rplidar" filename="libgazebo_ros_ray_sensor.so"> <!-- 加载雷达仿真插件 -->
<ros>
<namespace>/</namespace> <!-- 设置ROS话题的命名空间 -->
<remapping>~/out:=scan</remapping> <!-- 重映射激光话题 -->
</ros>
<output_type>sensor_msgs/LaserScan</output_type> <!-- 输出消息类型 -->
</plugin>
</sensor>
</gazebo>
运行
ros2 launch learning_gazebo load_mbot_laser_into_gazebo.launch.py # 启动加载激光雷达的仿真环境
ros2 run teleop_twist_keyboard teleop_twist_keyboard #键盘控制
ros2 run rviz2 rviz2 # 启动 Rviz 可视化工具
点击Add,选择Laserscan,配置Topic,rviz的固定坐标系为odom

4.6 Rqt
1. rqt 简介
- rqt 是一个基于 Qt 开发的模块化可视化工具,适用于 ROS 系统中的各种场景。
- 与 Rviz 不同,rqt 更注重提供小巧、模块化的功能插件,满足特定需求。
2. 常用功能
-
日志显示
1.1 运行
ros2 run rqt_console rqt_console # 启动日志显示工具 ros2 run turtlesim turtlesim_node # 启动小海龟仿真器1.2 记录器级别
|- Fatal:系统将终止以尝试保护自身免受损害
- Error:存在重大问题,这些问题不一定会损坏系统,但会妨碍系统正常运行
- Warn:意外的活动或不理想的结果,可能代表更深层次的问题,但不会彻底损害功能
- Info:指示事件和状态更新,作为系统是否按预期运行的视觉验证
- Debug:详细说明了系统执行的整个逐步过程
-
图像显示
-
发布话题数据/服务调用
-
绘制数据曲线
-
数据包管理
-
节点可视化
5. 资源汇总
5.1 常用框架
-
自主导航
Navigation2 是 ROS 2 中常用的自主导航框架,提供了从地图构建、定位、路径规划到避障的完整解决方案。
-
自动驾驶
Autoware 是一个基于 ROS 的开源自动驾驶软件,提供了从感知、定位、规划到控制的完整解决方案。
-
路径规划
机械臂路径规划 moveit 是 ROS 中常用的机械臂路径规划框架,支持多种机械臂模型和运动学求解器。
5.2 机器人学

- 《Principal of Robot Motion》——Howie Choset
- 《introduction to Robotics》——John J.Craig
- 《Robotics: Modelling, Planning and Control》——Bruno Siciliano
- 《Rigid Body Dynamics Algorithms》——Roy Featherstone
- 《Probabilistic Robotics》——Sebastian Thrun
- 《Handbook of Robotics》——Bruno Siciliano
- 《Robotic Manipulation》——Matthew T. Mason
- 《Robotics, Vision and Control》——Peter Corke
5.3 视频教程
5.4 常用链接
- ROS:https://www.ros.org
- ROS 2 Documentation:https://docs.ros.org/en/humble/index.html
- 古月居:https://www.guyuehome.com/
- ROS : https://www.ros.org
- ROS Wiki : http://wiki.ros.org/
- ROSCon : https://roscon.ros.org
- ROS Robots : https://robots.ros.org/
- Ubuntu Wiki : https://wiki.ubuntu.org.cn
- ROS2 Github : https://github.com/ros2
- Gazebo : https://classic.gazebosim.org

5939

被折叠的 条评论
为什么被折叠?



