第 0 章 ros学习前的所有操作
0.1 Ubuntu双系统的安装
在学习ros或opencv时,可能不可避免的需要使用Ubuntu系统,在这时我们通常有两种操作,1.通过虚拟机进行 2.通过双系统的安装进行,为了保证性能,一般推荐使用双系统。
0.1.1.制作Ubuntu启动盘
你需要一个容量大于8GB的u盘(制作启动盘会格式化u盘,所有东西都会丢失,建议使用空优盘),然后才能开始制作。
推荐使用rufus制作启动盘:Rufus - Create bootable USB drives the easy way
下载完后可以直接打开,我们还需要到ubuntu的官网去下载系统iso镜像:下载Ubuntu桌面系统 | Ubuntu
从个人的了解来看,如果学导航的话,目前ros1的学习建议下载Ubuntu 20.04.3 LTS,如果Ubuntu要用于视觉opencv(虽然Ubuntu自带opencv)或ros2去做项目的话一般下载才是Ubuntu 22.04 ,而且最新版才存在ros2.
镜像下载好之后,则打开rufus,按照以下步骤进行操作:

注:
1.设备会自动扫描你插入的u盘,如果你有多个u盘插入,请选择要写入镜像的u盘,别搞错了。
2.镜像文件选择下载好的Ubuntu iso系统镜像。
3.分区类型务必选择GPT分区表
4.目标类型务必选择UEFI(非CSM)
5.若你的BIOS只支持NTFS,要改成NTFS,不过一般都是支持FAT32的,先用FAT32,如果不行再回来改

然后点击开始,等待完成后,启动盘就制作好了。
0.1.2 进行磁盘管理分区
在Windows中打开磁盘管理
选择一个除C盘以外空间大的盘进行卷的压缩
例如:

在压缩空间量中选择合适的大小用于安装ubuntu。这里给出的建议是:
- 只是玩一玩linux系统,则分配30GB
- 学习ROS,80GB以上
- 深度学习、机器学习,100GB以上
- 软件开发,50GB
选择一个空间较多的盘,右键点击它,选择压缩:

压缩完后,会出现一个未分配内存,黑色部分的分区。

待安装Ubuntu系统的时候,我们将会对未分配区域(黑色区域)进行自定义分区。
0.1.3 进行Ubuntu系统的安装
将系统盘插入电脑中,然后重启电脑,这里需要进入bios界面更改为U盘启动(因为是通过U盘进行安装),不同品牌电脑进入bios的快捷键不同,一般为F2。
①重新启动电脑,开机时不停地按F2键进入bios界面
②更改优先启动项为U盘最高
个人:我是通过按F12切换启动方式进行安装,并没有改为U盘为优先启动项
完成切换U盘启动后,然后就会出现以下情况:

点击第一个就可以进行Ubuntu的安装了。
按个人需求设置语言,下划可以选择中文



这里一定更要选择其他选项,这样才可以自己分区:
不过如果你是在一台新电脑上安装(没有安装其他任何系统),你可以直接选择安装ubuntu,让它自己分区就好了。 |

我们将对该空闲空间对Ubuntu进行分区操作
例如,这是我双系统个人分区安装完后的分区情况:

原本大红框是我分的200G未分配内存,现在被分配成了自定义的Ubuntu系统分区
0.2 Ubuntu系统的删除
0.2.1 查看个人引导类型
1.win + R 打开运行窗口
2.输入msinfo32
这时就会弹出系统信息窗口

可以查看到我的个人引导BIOS模式是采用UEFI
注:一般BIOS都是采用的UEFI引导,也可能会有的是legacy引导,不同的引导移除方式不同
0.2.2 下载管理磁盘的工具
为了避免删除磁盘时出错,导致出现Windows等无法启动等错误,所以采用第三方软件进行操作。
管理磁盘工具:磁盘精灵
下载完后解压文件夹
打开文件夹中的以下程序

0.2.3 删除Ubuntu分区和EFI引导区
然后在这里删除Ubuntu的分区

最后,不能忘了删除以下Ubuntu部分的引导项
注:这里没删除Windows重启时会出现,grub命令行情况,输入exit才能跳出进入Windows

以上操作完后,重启就完成了,以后重启就没有以下的系统启动选项了,直接就是Windows

0.3 ros安装
这里ros安装推荐鱼香ros,简单又省时,输入以下代码入终端中
Plain Text wget http://fishros.com/install -O fishros && . fishros |
一键完成各种软件的安装,以上是旧版的鱼香ros操作,新版的鱼香ros有微信,qq等一键安装。


选择1,更换系统源是为了安装是速度更快。

目前学习ros1选择1
然后重复操作,下载VScode,未来编写代码都是在这
0.4 ros卸载
如果需要卸载ROS可以调用如下命令:
Plain Text sudo apt remove ros-noetic-* |
0.5 ros学习前的一些配置
0.5.1 配置ubuntu的软件和更新
配置ubuntu的软件和更新,允许安装不经认证的软件。
首先打开“软件和更新”对话框,具体可以在 Ubuntu 搜索按钮中搜索。
打开后按照下图进行配置(确保勾选了"restricted", "universe," 和 "multiverse.")

0.5.2 设置安装源
官方默认安装源:
Plain Text sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' |
或来自国内清华的安装源
Plain Text sudo sh -c '. /etc/lsb-release && echo "deb http://mirrors.tuna.tsinghua.edu.cn/ros/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/ros-latest.list' |
或来自国内中科大的安装源
Plain Text sudo sh -c '. /etc/lsb-release && echo "deb http://mirrors.ustc.edu.cn/ros/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/ros-latest.list' |
注:
1.回车后,可能需要输入管理员密码
2.建议使用国内资源,安装速度更快。
0.5.3 设置key
Plain Text sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 |
0.5.4 更新
需要更新 apt(以前是 apt-get, 官方建议使用 apt 而非 apt-get),apt 是用于从互联网仓库搜索、安装、升级、卸载软件或操作系统的工具。
Plain Text sudo apt update |
0.5.5 配置环境变量
配置环境变量,方便在任意 终端中使用 ROS
Plain Text echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc source ~/.bashrc |
0.6 ros测试
ROS 内置了一些小程序,可以通过运行这些小程序以检测 ROS 环境是否可以正常运行
- 首先启动三个命令行(ctrl + alt + T)
- 命令行1键入:roscore
- 命令行2键入:rosrun turtlesim turtlesim_node(此时会弹出图形化界面)
- 命令行3键入:rosrun turtlesim turtle_teleop_key(在3中可以通过上下左右控制2中乌龟的运动)
最终结果如下所示:

第 1 章 ros开发的基本操作和环境配置
1.1 未使用VScode时的ros编写的基础操作
- 先创建一个工作空间;
- 再创建一个功能包;
- 编辑源文件;
- 编辑配置文件;
- 编译并执行。
1.1.1 创建工作空间并初始化
Plaintext mkdir -p 自定义空间名称/src cd 自定义空间名称 catkin_make |
第一行:创建一个工作空间以及一个 src 子目录
第二行:进入工作空间
第三行:调用 catkin_make命令编译,生成devel、build。
devel:开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等
build:编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。
1.1.2 进入 src 创建 ros 包并添加依赖
Plain Text cd src catkin_create_pkg 自定义ROS包名 roscpp rospy std_msgs |
第一行:进入src目录
第二行:在工作空间下生成一个功能包,该功能包依赖于 roscpp、rospy 与 std_msgs
roscpp:使用C++实现的库
rospy:使用python实现的库
std_msgs:标准消息库
1.1.3 进入 ros 包的 src 目录编辑源文件
然后在ros包名中的src目录创建cpp文件,输入代码
C++ #include "ros/ros.h" int main(int argc, char *argv[]) { //执行 ros 节点初始化 ros::init(argc,argv,"hello"); //创建 ros 节点句柄(非必须) ros::NodeHandle nh; //控制台输出 hello world ROS_INFO("hello world!"); return 0; } |
1.1.4 编辑 ros 包下的 Cmakelist.txt文件
注:CMakeLists.txt是用于在build(避免目录混乱)生成Makefile文件来管理项目的
打开CMakeLists.txt,将其中以下代码进行添加,并修改
C++ add_executable(自定义的可执行文件名 src/自己创建源文件名.cpp ) target_link_libraries(自定义的可执行文件名 ${catkin_LIBRARIES} ) |
add_executable:将源文件映射成可执行文件
参数1:可执行程序名
参数2:源文件路径/源文件名称
target_link_libraries:
参数1:源文件|动态库文件|可执行文件
参数2:动态库
${catkin_LIBRARIES}:通过${}获取宏(catkin_LIBRARIES)中包含的动态库
1.1.5 进入工作空间目录并编译
C++ cd 自定义空间名称 catkin_make |
因为是通过CMakeLists.txt来编译的,所以需要到CMakeLists.txt所在目录
第一行:进入CMakeLists.txt所在目录(即自定义空间名称)
第二行:catkin_make编译CMakeLists.txt生成可执行文件(即自定义的可执行文件名(也看做一个节点))
生成 build devel ....
1.1.6 执行
先启动命令行1:
启动roscore,ros的核心
再启动命令行2:
C++ cd 工作空间 source ./devel/setup.bash rosrun 包名 C++节点 |
第一行:进入包含可执行文件(存在自定义的可执行文件名)的工作空间
第二行:目的就是在开新的terminal的时候,运行这个setup.bash,而这个setup.bash的作用是让一些ROS* 开头的命令可以使用。
注:工作空间的devel文件夹中存在几个setup.*sh形式的环境变量设置脚本。使用source命令运行这些脚本文件,则工作空间的环境变量设置可以生效
第三行:运行当前工作空间中的自定义功能包中的可执行文件(节点)
命令行输出: HelloWorld!
1.2 ros集成开发环境搭建
1.2.1 安装终端
在 ROS 中,需要频繁的使用到终端,且可能需要同时开启多个窗口,推荐一款较为好用的终端:Terminator。效果如下:

- 安装
C++ sudo apt install terminator |
- 添加到收藏夹
显示应用程序 ---> 搜索 terminator ---> 右击 选择 添加到收藏夹
- Terminator 常用快捷键
第一部份:关于在同一个标签内的操作
Plain Text Alt+Up //移动到上面的终端 Alt+Down //移动到下面的终端 Alt+Left //移动到左边的终端 Alt+Right //移动到右边的终端 Ctrl+Shift+O //水平分割终端 Ctrl+Shift+E //垂直分割终端 Ctrl+Shift+Right //在垂直分割的终端中将分割条向右移动 Ctrl+Shift+Left //在垂直分割的终端中将分割条向左移动 Ctrl+Shift+Up //在水平分割的终端中将分割条向上移动 Ctrl+Shift+Down //在水平分割的终端中将分割条向下移动 Ctrl+Shift+S //隐藏/显示滚动条 Ctrl+Shift+F //搜索 Ctrl+Shift+C //复制选中的内容到剪贴板 Ctrl+Shift+V //粘贴剪贴板的内容到此处 Ctrl+Shift+W //关闭当前终端 Ctrl+Shift+Q //退出当前窗口,当前窗口的所有终端都将被关闭 Ctrl+Shift+X //最大化显示当前终端 Ctrl+Shift+Z //最大化显示当前终端并使字体放大 Ctrl+Shift+N or Ctrl+Tab //移动到下一个终端 Ctrl+Shift+P or Ctrl+Shift+Tab //Crtl+Shift+Tab 移动到之前的一个终端 |
第二部份:有关各个标签之间的操作
Plain Text F11 //全屏开关 Ctrl+Shift+T //打开一个新的标签 Ctrl+PageDown //移动到下一个标签 Ctrl+PageUp //移动到上一个标签 Ctrl+Shift+PageDown //将当前标签与其后一个标签交换位置 Ctrl+Shift+PageUp //将当前标签与其前一个标签交换位置 Ctrl+Plus (+) //增大字体 Ctrl+Minus (-) //减小字体 Ctrl+Zero (0) //恢复字体到原始大小 Ctrl+Shift+R //重置终端状态 Ctrl+Shift+G //重置终端状态并clear屏幕 Super+g //绑定所有的终端,以便向一个输入能够输入到所有的终端 Super+Shift+G //解除绑定 Super+t //绑定当前标签的所有终端,向一个终端输入的内容会自动输入到其他终端 Super+Shift+T //解除绑定 Ctrl+Shift+I //打开一个窗口,新窗口与原来的窗口使用同一个进程 Super+i //打开一个新窗口,新窗口与原来的窗口使用不同的进程 |
1.2.2 安装VScode
可以在鱼香ros中直接安装VScode,省时省力
Plain Text wget http://fishros.com/install -O fishros && . fishros |
1.2.2.1 下载
vscode 下载:Documentation for Visual Studio Code
历史版本下载链接: Visual Studio Code May 2024
1.2.2.2 vscode 安装与卸载
1.2.2.2.1 安装
方式1:双击安装即可(或右击选择安装)
方式2:sudo dpkg -i xxxx.deb
1.2.2.2.2 卸载
Plaintext sudo dpkg --purge code |
1.2.2.3 vscode 集成 ROS 插件
使用 VScode 开发 ROS 程序,需要先安装一些插件,常用插件如下:


1.2.3 vscode 使用_基本配置
1.2.3.1 创建 ROS 工作空间
Plaintext mkdir -p xxx_ws/src(必须得有 src) cd xxx_ws catkin_make |
1.2.3.2 启动 vscode
进入 xxx_ws 启动 vscode
Plaintext cd xxx_ws code . |
1.2.3.3 vscode 中编译 ros
快捷键 ctrl + shift + B 调用编译,选择:catkin_make:build
可以点击配置设置为默认,修改.vscode/tasks.json 文件
C++ { // 有关 tasks.json 格式的文档,请参见 // https://go.microsoft.com/fwlink/?LinkId=733558 "version": "2.0.0", "tasks": [ { "label": "catkin_make:debug", //代表提示的描述性信息 "type": "shell", //可以选择shell或者process,如果是shell代码是在shell里面运行一个命令,如果是process代表作为一个进程来运行 "command": "catkin_make",//这个是我们需要运行的命令 "args": [],//如果需要在命令后面加一些后缀,可以写在这里,比如-DCATKIN_WHITELIST_PACKAGES=“pac1;pac2” "group": {"kind":"build","isDefault":true}, "presentation": { "reveal": "always"//可选always或者silence,代表是否输出信息 }, "problemMatcher": "$msCompile" } ] } |
1.2.3.4 创建 ROS 功能包
选定 src 右击 ---> create catkin package
设置包名 添加依赖
1.2.3.5 C++ 实现
在功能包的 src 下新建 cpp 文件
C++ /*控制台输出 HelloVSCode !!!*/ include "ros/ros.h" int main(int argc, char *argv[]) { setlocale(LC_ALL,"");//执行节点初始化 ros::init(argc,argv,"HelloVSCode"); //输出日志 ROS_INFO("Hello VSCode!!!哈哈哈哈哈哈哈哈哈哈");return 0; } |
1.2.3.6 配置 CMakeLists.txt
C++ 配置:
CMake add_executable(节点名称 src/C++源文件名.cpp ) target_link_libraries(节点名称 ${catkin_LIBRARIES} ) |
1.2.3.7 编译执行
编译: ctrl + shift + B
执行: 和之前一致,只是可以在 VScode 中添加终端,首先执行:source ./devel/setup.bash
1.3 VS无法create catkin package问题

Create a ROS Workspace
Let's create a catkin workspace:
Plain Text $ mkdir -p ~/catkin_ws/src $ cd ~/catkin_ws/src $ catkin_init_workspace |
Even though the workspace is empty (there are no packages in the 'src' folder, just a singleCMakeLists.txt link) you can still "build" the workspace:
Plain Text $ cd ~/catkin_ws/ $ catkin_make |
The catkin_make command is a convenience tool for working with catkin workspaces. If you look in your current directory you should now have a 'build' and 'devel' folder. Inside the 'devel' folder you can see that there are now several setup.*sh files. Sourcing any of these files will overlay this workspace on top of your environment. To understand more about this see the general catkin documentation:catkin. Before continuing source your new setup.*sh file:
Plain Text $ source devel/setup.bash |
To make sure your workspace is properly overlayed by the setup script, make sureROS_PACKAGE_PATH environment variable includes the directory you're in.
Plain Text $ echo $$ROS_PACKAGE_PATH |
/home/youruser/catkin_ws/src:/opt/ros/indigo/share:/opt/ros/indigo/stacks
总结
Plain Text mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_init_workspace cd ~/catkin_ws/ catkin_make source devel/setup.bash echo $$ROS_PACKAGE_PATH |
或:
CMake mkdir -p ~/catkin_ws/src cd ~/catkin_ws/ catkin_make source devel/setup.bash cd ~/catkin_ws/src catkin_create_pkg beginner_tutorials std_msgs rospy roscpp |
完成以上操作就可以在VScode中进行create catkin package
1.4 ROS框架详解
1.4.1 ROS文件系统
ROS文件系统级指的是在硬盘上ROS源代码的组织形式,其结构大致可以如下图所示:

Plaintext WorkSpace --- 自定义的工作空间 |--- build:编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。 |--- devel:开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等。 |--- src: 源码 |-- package:功能包(ROS基本单元)包含多个节点、库与配置文件,包名所有字母小写,只能由字母、数字与下划线组成 |-- CMakeLists.txt 配置编译规则,比如源文件、依赖项、目标文件 |-- package.xml 包信息,比如:包名、版本、作者、依赖项...(以前版本是 manifest.xml) |-- scripts 存储python文件 |-- src 存储C++源文件 |-- include 头文件 |-- msg 消息通信格式文件 |-- srv 服务通信格式文件 |-- action 动作格式文件 |-- launch 可一次性运行多个节点 |-- config 配置信息 |-- CMakeLists.txt: 编译的基本配置 |
1.package.xml
该文件定义有关软件包的属性,例如软件包名称,版本号,作者,维护者以及对其他catkin软件包的依赖性。请注意,该概念类似于旧版 rosbuild 构建系统中使用的manifest.xml文件。
JavaScript <?xml version="1.0"?> <!-- 格式: 以前是 1,推荐使用格式 2 --> <package format="2"> <!-- 包名 --> <name>demo01_hello_vscode</name> <!-- 版本 --> <version>0.0.0</version> <!-- 描述信息 --> <description>The demo01_hello_vscode package</description>
<!-- One maintainer tag required, multiple allowed, one person per tag --> <!-- Example: --> <!-- <maintainer email="jane.doe@example.com">Jane Doe</maintainer> --> <!-- 维护人员 --> <maintainer email="xuzuo@todo.todo">xuzuo</maintainer>
<!-- One license tag required, multiple allowed, one license per tag --> <!-- Commonly used license strings: --> <!-- BSD, MIT, Boost Software License, GPLv2, GPLv3, LGPLv2.1, LGPLv3 --> <!-- 许可证信息,ROS核心组件默认 BSD --> <license>TODO</license>
<!-- Url tags are optional, but multiple are allowed, one per tag --> <!-- Optional attribute type can be: website, bugtracker, or repository --> <!-- Example: --> <!-- <url type="website">http://wiki.ros.org/demo01_hello_vscode</url> -->
<!-- Author tags are optional, multiple are allowed, one per tag --> <!-- Authors do not have to be maintainers, but could be --> <!-- Example: --> <!-- <author email="jane.doe@example.com">Jane Doe</author> -->
<!-- The *depend tags are used to specify dependencies --> <!-- Dependencies can be catkin packages or system dependencies --> <!-- Examples: --> <!-- Use depend as a shortcut for packages that are both build and exec dependencies --> <!-- <depend>roscpp</depend> --> <!-- Note that this is equivalent to the following: --> <!-- <build_depend>roscpp</build_depend> --> <!-- <exec_depend>roscpp</exec_depend> --> <!-- Use build_depend for packages you need at compile time: --> <!-- <build_depend>message_generation</build_depend> --> <!-- Use build_export_depend for packages you need in order to build against this package: --> <!-- <build_export_depend>message_generation</build_export_depend> --> <!-- Use buildtool_depend for build tool packages: --> <!-- <buildtool_depend>catkin</buildtool_depend> --> <!-- Use exec_depend for packages you need at runtime: --> <!-- <exec_depend>message_runtime</exec_depend> --> <!-- Use test_depend for packages you need only for testing: --> <!-- <test_depend>gtest</test_depend> --> <!-- Use doc_depend for packages you need only for building documentation: --> <!-- <doc_depend>doxygen</doc_depend> --> <!-- 依赖的构建工具,这是必须的 --> <buildtool_depend>catkin</buildtool_depend>
<!-- 指定构建此软件包所需的软件包 --> <build_depend>roscpp</build_depend> <build_depend>rospy</build_depend> <build_depend>std_msgs</build_depend>
<!-- 指定根据这个包构建库所需要的包 --> <build_export_depend>roscpp</build_export_depend> <build_export_depend>rospy</build_export_depend> <build_export_depend>std_msgs</build_export_depend>
<!-- 运行该程序包中的代码所需的程序包 --> <exec_depend>roscpp</exec_depend> <exec_depend>rospy</exec_depend> <exec_depend>std_msgs</exec_depend>
<!-- The export tag contains other, unspecified, tags --> <export> <!-- Other tools can request additional information be placed here -->
</export> </package> |
2.CMakelists.txt
文件CMakeLists.txt是CMake构建系统的输入,用于构建软件包。任何兼容CMake的软件包都包含一个或多个CMakeLists.txt文件,这些文件描述了如何构建代码以及将代码安装到何处。
CMake cmake_minimum_required(VERSION 3.0.2) #所需 cmake 版本 project(demo01_hello_vscode) #包名称,会被 ${PROJECT_NAME} 的方式调用
## Compile as C++11, supported in ROS Kinetic and newer # add_compile_options(-std=c++11)
## Find catkin macros and libraries ## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz) ## is used, also find other catkin packages #设置构建所需要的软件包 find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs )
## System dependencies are found with CMake's conventions #默认添加系统依赖 # find_package(Boost REQUIRED COMPONENTS system)
## Uncomment this if the package has a setup.py. This macro ensures ## modules and global scripts declared therein get installed ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html # 启动 python 模块支持 # catkin_python_setup()
################################################ ## Declare ROS messages, services and actions ## ## 声明 ROS 消息、服务、动作... ## ################################################
## To declare and build messages, services or actions from within this ## package, follow these steps: ## * Let MSG_DEP_SET be the set of packages whose message types you use in ## your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...). ## * In the file package.xml: ## * add a build_depend tag for "message_generation" ## * add a build_depend and a exec_depend tag for each package in MSG_DEP_SET ## * If MSG_DEP_SET isn't empty the following dependency has been pulled in ## but can be declared for certainty nonetheless: ## * add a exec_depend tag for "message_runtime" ## * In this file (CMakeLists.txt): ## * add "message_generation" and every package in MSG_DEP_SET to ## find_package(catkin REQUIRED COMPONENTS ...) ## * add "message_runtime" and every package in MSG_DEP_SET to ## catkin_package(CATKIN_DEPENDS ...) ## * uncomment the add_*_files sections below as needed ## and list every .msg/.srv/.action file to be processed ## * uncomment the generate_messages entry below ## * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...)
## Generate messages in the 'msg' folder # add_message_files( # FILES # Message1.msg # Message2.msg # )
## Generate services in the 'srv' folder # add_service_files( # FILES # Service1.srv # Service2.srv # )
## Generate actions in the 'action' folder # add_action_files( # FILES # Action1.action # Action2.action # )
## Generate added messages and services with any dependencies listed here # 生成消息、服务时的依赖包 # generate_messages( # DEPENDENCIES # std_msgs # )
################################################ ## Declare ROS dynamic reconfigure parameters ## ## 声明 ROS 动态参数配置 ## ################################################
## To declare and build dynamic reconfigure parameters within this ## package, follow these steps: ## * In the file package.xml: ## * add a build_depend and a exec_depend tag for "dynamic_reconfigure" ## * In this file (CMakeLists.txt): ## * add "dynamic_reconfigure" to ## find_package(catkin REQUIRED COMPONENTS ...) ## * uncomment the "generate_dynamic_reconfigure_options" section below ## and list every .cfg file to be processed
## Generate dynamic reconfigure parameters in the 'cfg' folder # generate_dynamic_reconfigure_options( # cfg/DynReconf1.cfg # cfg/DynReconf2.cfg # )
################################### ## catkin specific configuration ## ## catkin 特定配置## ################################### ## The catkin_package macro generates cmake config files for your package ## Declare things to be passed to dependent projects ## INCLUDE_DIRS: uncomment this if your package contains header files ## LIBRARIES: libraries you create in this project that dependent projects also need ## CATKIN_DEPENDS: catkin_packages dependent projects also need ## DEPENDS: system dependencies of this project that dependent projects also need # 运行时依赖 catkin_package( # INCLUDE_DIRS include # LIBRARIES demo01_hello_vscode # CATKIN_DEPENDS roscpp rospy std_msgs # DEPENDS system_lib )
########### ## Build ## ###########
## Specify additional locations of header files ## Your package locations should be listed before other locations # 添加头文件路径,当前程序包的头文件路径位于其他文件路径之前 include_directories( # include ${catkin_INCLUDE_DIRS} )
## Declare a C++ library # 声明 C++ 库 # add_library(${PROJECT_NAME} # src/${PROJECT_NAME}/demo01_hello_vscode.cpp # )
## Add cmake target dependencies of the library ## as an example, code may need to be generated before libraries ## either from message generation or dynamic reconfigure # 添加库的 cmake 目标依赖 # add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
## Declare a C++ executable ## With catkin_make all packages are built within a single CMake context ## The recommended prefix ensures that target names across packages don't collide # 声明 C++ 可执行文件 add_executable(Hello_VSCode src/Hello_VSCode.cpp)
## Rename C++ executable without prefix ## The above recommended prefix causes long target names, the following renames the ## target back to the shorter version for ease of user use ## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node" #重命名c++可执行文件 # set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "")
## Add cmake target dependencies of the executable ## same as for the library above #添加可执行文件的 cmake 目标依赖 add_dependencies(Hello_VSCode ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
## Specify libraries to link a library or executable target against #指定库、可执行文件的链接库 target_link_libraries(Hello_VSCode ${catkin_LIBRARIES} )
############# ## Install ## ## 安装 ## #############
# all install targets should use catkin DESTINATION variables # See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html
## Mark executable scripts (Python etc.) for installation ## in contrast to setup.py, you can choose the destination #设置用于安装的可执行脚本 catkin_install_python(PROGRAMS scripts/Hi.py DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
## Mark executables for installation ## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_executables.html # install(TARGETS ${PROJECT_NAME}_node # RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} # )
## Mark libraries for installation ## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_libraries.html # install(TARGETS ${PROJECT_NAME} # ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} # LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} # RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} # )
## Mark cpp header files for installation # install(DIRECTORY include/${PROJECT_NAME}/ # DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} # FILES_MATCHING PATTERN "*.h" # PATTERN ".svn" EXCLUDE # )
## Mark other files for installation (e.g. launch and bag files, etc.) # install(FILES # # myfile1 # # myfile2 # DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} # )
############# ## Testing ## #############
## Add gtest based cpp test target and link libraries # catkin_add_gtest(${PROJECT_NAME}-test test/test_demo01_hello_vscode.cpp) # if(TARGET ${PROJECT_NAME}-test) # target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}) # endif()
## Add folders to be run by python nosetests # catkin_add_nosetests(test) |
1.4.2 ROS文件系统相关命令
ROS 的文件系统本质上都还是操作系统文件,我们可以使用Linux命令来操作这些文件,不过,在ROS中为了更好的用户体验,ROS专门提供了一些类似于Linux的命令,这些命令较之于Linux原生命令,更为简介、高效。文件操作,无外乎就是增删改查与执行等操作,接下来,我们就从这五个维度,来介绍ROS文件系统的一些常用命令。
1.增
catkin_create_pkg 自定义包名 依赖包 === 创建新的ROS功能包
sudo apt install xxx === 安装 ROS功能包
2.删
sudo apt purge xxx ==== 删除某个功能包
3.查
rospack list === 列出所有功能包
rospack find 包名 === 查找某个功能包是否存在,如果存在返回安装路径
roscd 包名 === 进入某个功能包
rosls 包名 === 列出某个包下的文件
apt search xxx === 搜索某个功能包
4.改
rosed 包名 文件名 === 修改功能包文件
需要安装 vim
比如:rosed turtlesim Color.msg
5.执行
5.1roscore
roscore === 是 ROS 的系统先决条件节点和程序的集合, 必须运行 roscore 才能使 ROS 节点进行通信。
roscore 将启动:
用法:
或(指定端口号)
5.2rosrun
rosrun 包名 可执行文件名 === 运行指定的ROS节点
比如:rosrun turtlesim turtlesim_node
5.3roslaunch
roslaunch 包名 launch文件名 === 执行某个包下的 launch 文件
1.4.3 ROS计算图
1.计算图简介
前面介绍的是ROS文件结构,是磁盘上 ROS 程序的存储结构,是静态的,而 ros 程序运行之后,不同的节点之间是错综复杂的,ROS 中提供了一个实用的工具:rqt_graph。
rqt_graph能够创建一个显示当前系统运行情况的动态图形。ROS 分布式系统中不同进程需要进行数据交互,计算图可以以点对点的网络形式表现数据交互过程。rqt_graph是rqt程序包中的一部分。
2.计算图安装
如果前期把所有的功能包(package)都已经安装完成,则直接在终端窗口中输入
rosrun rqt_graph rqt_graph
如果未安装则在终端(terminal)中输入
Plaintext $ sudo apt install ros-<distro>-rqt $ sudo apt install ros-<distro>-rqt-common-plugins |
请使用你的ROS版本名称(比如:kinetic、melodic、Noetic等)来替换掉<distro>。
例如当前版本是 Noetic,就在终端窗口中输入
Plaintext $ sudo apt install ros-noetic-rqt $ sudo apt install ros-noetic-rqt-common-plugins |
3.计算图演示
接下来以 ROS 内置的小乌龟案例来演示计算图
首先,按照前面所示,运行案例
然后,启动新终端,键入: rqt_graph 或 rosrun rqt_graph rqt_graph,可以看到类似下图的网络拓扑图,该图可以显示不同节点之间的关系。
第 2 章 ROS通信机制
2.1 话题通讯
2.1.1 理论原型
话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。

整个流程由以下步骤实现:
0.Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。
3.Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
4.Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
5.Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
6.Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议
注意2: Talker 与 Listener 的启动无先后顺序要求
注意3: Talker 与 Listener 都可以有多个
注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。
2.1.2 话题通信基本操作
需求:
编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出。 |
分析:
在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
- 发布方
- 接收方
- 数据(此处为普通文本)
流程:
- 编写发布方实现;
- 编写订阅方实现;
- 编辑配置文件;
- 编译并执行。
1.发布方
C++ /* 需求: 实现基本的话题通信,一方发布数据,一方接收数据, 实现的关键点: 1.发送方 2.接收方 3.数据(此处为普通文本) PS: 二者需要设置相同的话题 消息发布方: 循环发布信息:HelloWorld 后缀数字编号 实现流程: 1.包含头文件 2.初始化 ROS 节点:命名(唯一) 3.实例化 ROS 句柄 4.实例化 发布者 对象 5.组织被发布的数据,并编写逻辑发布数据 */ // 1.包含头文件 include "ros/ros.h" include "std_msgs/String.h" //普通文本类型的消息 include <sstream> int main(int argc, char *argv[]) { //设置编码 setlocale(LC_ALL,""); //2.初始化 ROS 节点:命名(唯一)// 参数1和参数2 后期为节点传值会使用// 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一 ros::init(argc,argv,"talker");//3.实例化 ROS 句柄 ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能//4.实例化 发布者 对象//泛型: 发布的消息类型//参数1: 要发布到的话题//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁) ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10); //5.组织被发布的数据,并编写逻辑发布数据//数据(动态组织) std_msgs::String msg;// msg.data = "你好啊!!!";std::string msg_front = "Hello 你好!"; //消息前缀int count = 0; //消息计数器//逻辑(一秒10次) ros::Rate r(1); //节点不死 while (ros::ok()) { //使用 stringstream 拼接字符串与编号std::stringstream ss; ss << msg_front << count; msg.data = ss.str();//发布消息 pub.publish(msg);//加入调试,打印发送的消息 ROS_INFO("发送的消息:%s",msg.data.c_str()); //根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率; r.sleep(); count++;//循环结束前,让 count 自增//暂无应用 ros::spinOnce(); } return 0; } |
2.订阅方
C++ /* 需求: 实现基本的话题通信,一方发布数据,一方接收数据, 实现的关键点: 1.发送方 2.接收方 3.数据(此处为普通文本) 消息订阅方: 订阅话题并打印接收到的消息 实现流程: 1.包含头文件 2.初始化 ROS 节点:命名(唯一) 3.实例化 ROS 句柄 4.实例化 订阅者 对象 5.处理订阅的消息(回调函数) 6.设置循环调用回调函数 */ // 1.包含头文件 include "ros/ros.h" include "std_msgs/String.h" void doMsg(const std_msgs::String::ConstPtr& msg_p) { ROS_INFO("我听见:%s",msg_p->data.c_str());// ROS_INFO("我听见:%s",(*msg_p).data.c_str()); } int main(int argc, char *argv[]) { setlocale(LC_ALL,"");//2.初始化 ROS 节点:命名(唯一) ros::init(argc,argv,"listener");//3.实例化 ROS 句柄 ros::NodeHandle nh; //4.实例化 订阅者 对象 ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);//5.处理订阅的消息(回调函数)// 6.设置循环调用回调函数 ros::spin();//循环读取接收的数据,并调用回调函数处理return 0; } |
3.配置 CMakeLists.txt
CMake add_executable(Hello_pub src/Hello_pub.cpp ) add_executable(Hello_sub src/Hello_sub.cpp )
target_link_libraries(Hello_pub ${catkin_LIBRARIES} ) target_link_libraries(Hello_sub ${catkin_LIBRARIES} ) |
4.执行
1.启动 roscore;
2.启动发布节点;
3.启动订阅节点。
运行结果与引言部分的演示案例1类似。
5.注意
补充0:
vscode 中的 main 函数 声明 int main(int argc, char const *argv[]){},默认生成 argv 被 const 修饰,需要去除该修饰符
补充1:
ros/ros.h No such file or directory .....
检查 CMakeList.txt find_package 出现重复,删除内容少的即可
参考资料:fatal error: ros/ros.h: No such file or directory - ROS Answers: Open Source Q&A Forum
补充2:
find_package 不添加一些包,也可以运行啊, ros.wiki 答案如下
Plaintext You may notice that sometimes your project builds fine even if you did not call find_package with all dependencies. This is because catkin combines all your projects into one, so if an earlier project calls find_package, yours is configured with the same values. But forgetting the call means your project can easily break when built in isolation. |
补充3:
订阅时,第一条数据丢失
原因: 发送第一条数据时, publisher 还未在 roscore 注册完毕
解决: 注册后,加入休眠 ros::Duration(3.0).sleep(); 延迟第一条数据的发送
PS:可以使用 rqt_graph 查看节点关系。
2.1.3 话题通信自定义msg
在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty.... 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息... std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型
msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:
- int8, int16, int32, int64 (或者无符号类型: uint*)
- variable-length array[] and fixed-length array[C]
ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。
需求:创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。
流程:
- 按照固定格式创建 msg 文件
- 编辑配置文件
- 编译生成可以被 Python 或 C++ 调用的中间文件
1.定义msg文件
功能包下新建 msg 目录,添加文件 Person.msg
Plaintext string name uint16 age float64 height |
2.编辑配置文件
package.xml中添加编译依赖与执行依赖
CMake <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend> <!-- exce_depend 以前对应的是 run_depend 现在非法 --> |
CMakeLists.txt编辑 msg 相关配置
CMake find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs |
CMake ## 配置 msg 源文件 add_message_files( FILES Person.msg ) |
CMake # 生成消息时依赖于 std_msgs generate_messages( DEPENDENCIES std_msgs ) |
CMake #执行时依赖 catkin_package( INCLUDE_DIRS include LIBRARIES demo02_talker_listener CATKIN_DEPENDS roscpp rospy std_msgs message_runtime DEPENDS system_lib ) |
3.编译
编译后的中间文件查看:
C++ 需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)

Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/msg)

后续调用相关 msg 时,是从这些中间文件调用的
2.1.4 话题通信自定义msg调用
需求:
编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。 |
分析:
在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
- 发布方
- 接收方
- 数据(此处为自定义消息)
流程:
- 编写发布方实现;
- 编写订阅方实现;
- 编辑配置文件;
- 编译并执行。
0.vscode 配置
为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:
Plaintext { "configurations": [ { "browse": { "databaseFilename": "", "limitSymbolsToIncludedHeaders": true }, "includePath": [ "/opt/ros/noetic/include/**", "/usr/include/**", "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17" } ], "version": 4 } |
1.发布方
C++ /* 需求: 循环发布人的信息
*/
#include "ros/ros.h" #include "demo02_talker_listener/Person.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,"");
//1.初始化 ROS 节点 ros::init(argc,argv,"talker_person");
//2.创建 ROS 句柄 ros::NodeHandle nh;
//3.创建发布者对象 ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);
//4.组织被发布的消息,编写发布逻辑并发布消息 demo02_talker_listener::Person p; p.name = "sunwukong"; p.age = 2000; p.height = 1.45;
ros::Rate r(1); while (ros::ok()) { pub.publish(p); p.age += 1; ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);
r.sleep(); ros::spinOnce(); }
return 0; } |
2.订阅方
C++ /* 需求: 订阅人的信息
*/
#include "ros/ros.h" #include "demo02_talker_listener/Person.h"
void doPerson(const demo02_talker_listener::Person::ConstPtr& person_p) { ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height); }
int main(int argc, char *argv[]) { setlocale(LC_ALL,"");
//1.初始化 ROS 节点 ros::init(argc,argv,"listener_person"); //2.创建 ROS 句柄 ros::NodeHandle nh; //3.创建订阅对象 ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson);
//4.回调函数中处理 person
//5.ros::spin(); ros::spin(); return 0; } |
3.配置 CMakeLists.txt
需要添加 add_dependencies 用以设置所依赖的消息相关的中间文件。
CMake add_executable(person_talker src/person_talker.cpp) add_executable(person_listener src/person_listener.cpp)
add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp) add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp)
target_link_libraries(person_talker${catkin_LIBRARIES} ) target_link_libraries(person_listener${catkin_LIBRARIES} ) |
4.执行
1.启动 roscore;
2.启动发布节点;
3.启动订阅节点。
运行结果与引言部分的演示案例2类似。
PS:可以使用 rqt_graph 查看节点关系。
2.2 服务通信
服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:
机器人巡逻过程中,控制系统分析传感器数据发现可疑物体或人... 此时需要拍摄照片并留存。
在上述场景中,就使用到了服务通信。
- 一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果
与上述应用类似的,服务通信更适用于对时时性有要求、具有一定逻辑处理的应用场景。
概念
以请求响应的方式实现不同节点之间数据交互的通信模式。
作用
用于偶然的、对时时性有要求、有一定逻辑处理需求的数据传输场景。
案例
实现两个数字的求和,客户端节点,运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端。

另请参考:
2.2.1 服务通信理论模型
服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:
ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

整个流程由以下步骤实现:
0.Server注册
Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
1.Client注册
Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。
2.ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。
3.Client发送请求
Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。
4.Server发送响应
Server 接收、解析请求的数据,并产生响应结果返回给 Client。 注意: 1.客户端请求被处理时,需要保证服务器已经启动; 2.服务端和客户端都可以存在多个。 |
2.2.2 服务通信自定义srv
需求:
服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。 |
流程:
srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:
- 按照固定格式创建srv文件
- 编辑配置文件
- 编译生成中间文件
1.定义srv文件
功能包下新建 srv 目录,添加 xxx.srv 文件,内容:
C++ # 客户端请求时发送的两个数字 int32 num1 int32 num2 --- # 服务器响应发送的数据 int32 sum |
2.编辑配置文件
package.xml中添加编译依赖与执行依赖
Plaintext <build_depend>message_generation</build_depend> <exec_depend>message_runtime</exec_depend> <!-- exce_depend 以前对应的是 run_depend 现在非法 --> |
CMakeLists.txt编辑 srv 相关配置
CMake find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation ) # 需要加入 message_generation,必须有 std_msgs |
Plaintext add_service_files( FILES AddInts.srv ) |
Plaintext generate_messages( DEPENDENCIES std_msgs ) |
注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以
3.编译
编译后的中间文件查看:
C++ 需要调用的中间文件(.../工作空间/devel/include/包名/xxx.h)

Python 需要调用的中间文件(.../工作空间/devel/lib/python3/dist-packages/包名/srv)

后续调用相关 srv 时,是从这些中间文件调用的
2.2.3 服务通信自定义srv调用
需求:
编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。 |
分析:
在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:
- 服务端
- 客户端
- 数据
流程:
- 编写服务端实现;
- 编写客户端实现;
- 编辑配置文件;
- 编译并执行。
0.vscode配置
需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:
Plaintext { "configurations": [ { "browse": { "databaseFilename": "", "limitSymbolsToIncludedHeaders": true }, "includePath": [ "/opt/ros/noetic/include/**", "/usr/include/**", "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17" } ], "version": 4 } |
1.服务端
C++ /* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析
服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 服务 对象 5.回调函数处理请求并产生响应 6.由于请求有多个,需要调用 ros::spin()
*/ #include "ros/ros.h" #include "demo03_server_client/AddInts.h"
// bool 返回值由于标志是否处理成功 bool doReq(demo03_server_client::AddInts::Request& req, demo03_server_client::AddInts::Response& resp){ int num1 = req.num1; int num2 = req.num2;
ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);
//逻辑处理 if (num1 < 0 || num2 < 0) { ROS_ERROR("提交的数据异常:数据不可以为负数"); return false; }
//如果没有异常,那么相加并将结果赋值给 resp resp.sum = num1 + num2; return true;
}
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Server"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 服务 对象 ros::ServiceServer server = nh.advertiseService("AddInts",doReq); ROS_INFO("服务已经启动...."); // 5.回调函数处理请求并产生响应 // 6.由于请求有多个,需要调用 ros::spin() ros::spin(); return 0; } |
2.客户端
C++ /* 需求: 编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器 服务器需要解析客户端提交的数据,相加后,将结果响应回客户端, 客户端再解析
服务器实现: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建 客户端 对象 5.请求服务,接收响应
*/ // 1.包含头文件 #include "ros/ros.h" #include "demo03_server_client/AddInts.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,"");
// 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3 if (argc != 3) // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径) { ROS_ERROR("请提交两个整数"); return 1; }
// 2.初始化 ROS 节点 ros::init(argc,argv,"AddInts_Client"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建 客户端 对象 ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts"); //等待服务启动成功 //方式1 ros::service::waitForService("AddInts"); //方式2 // client.waitForExistence(); // 5.组织请求数据 demo03_server_client::AddInts ai; ai.request.num1 = atoi(argv[1]); ai.request.num2 = atoi(argv[2]); // 6.发送请求,返回 bool 值,标记是否成功 bool flag = client.call(ai); // 7.处理响应 if (flag) { ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum); } else { ROS_ERROR("请求处理失败...."); return 1; }
return 0; } |
3.配置 CMakeLists.txt
Plaintext add_executable(AddInts_Server src/AddInts_Server.cpp) add_executable(AddInts_Client src/AddInts_Client.cpp) add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp) add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp) target_link_libraries(AddInts_Server ${catkin_LIBRARIES} ) target_link_libraries(AddInts_Client ${catkin_LIBRARIES} ) |
4.执行
流程:
- 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2
结果:
会根据提交的数据响应相加后的结果。
注意:
如果先启动客户端,那么会导致运行失败
优化:
在客户端发送请求前添加:client.waitForExistence();
或:ros::service::waitForService("AddInts");
这是一个阻塞式函数,只有服务启动成功后才会继续执行
此处可以使用 launch 文件优化,但是需要注意 args 传参特点
2.3 参数服务器
参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,关于参数服务器的典型应用场景如下:
导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径
上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:
- 路径规划时,需要参考小车的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数
参数服务器,一般适用于存在数据共享的一些应用场景。
概念
以共享的方式实现不同节点之间数据交互的通信模式。
作用
存储一些多节点共享的数据,类似于全局变量。
案例
实现参数增删改查操作。
另请参考:
2.3.1 参数服务器理论模型
参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:
ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。

整个流程由以下步骤实现:
1.Talker 设置参数
Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。
2.Listener 获取参数
Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。
3.ROS Master 向 Listener 发送参数值
ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。
参数可使用数据类型:
- base64-encoded binary data
注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据 |
2.3.2 参数操作
需求:实现参数服务器参数的增删改查操作。
在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:
下面为具体操作演示
1.参数服务器新增(修改)参数
C++ /* 参数服务器操作之新增与修改(二者API一样)_C++实现: 在 roscpp 中提供了两套 API 实现参数操作 ros::NodeHandle setParam("键",值) ros::param set("键","值")
示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数 修改(相同的键,不同的值)
*/ #include "ros/ros.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"set_update_param");
std::vector<std::string> stus; stus.push_back("zhangsan"); stus.push_back("李四"); stus.push_back("王五"); stus.push_back("孙大脑袋");
std::map<std::string,std::string> friends; friends["guo"] = "huang"; friends["yuang"] = "xiao";
//NodeHandle-------------------------------------------------------- ros::NodeHandle nh; nh.setParam("nh_int",10); //整型 nh.setParam("nh_double",3.14); //浮点型 nh.setParam("nh_bool",true); //bool nh.setParam("nh_string","hello NodeHandle"); //字符串 nh.setParam("nh_vector",stus); // vector nh.setParam("nh_map",friends); // map
//修改演示(相同的键,不同的值) nh.setParam("nh_int",10000);
//param-------------------------------------------------------- ros::param::set("param_int",20); ros::param::set("param_double",3.14); ros::param::set("param_string","Hello Param"); ros::param::set("param_bool",false); ros::param::set("param_vector",stus); ros::param::set("param_map",friends);
//修改演示(相同的键,不同的值) ros::param::set("param_int",20000);
return 0; } |
2.参数服务器获取参数
C++ /* 参数服务器操作之查询_C++实现: 在 roscpp 中提供了两套 API 实现参数操作 ros::NodeHandle
param(键,默认值) 存在,返回对应结果,否则返回默认值
getParam(键,存储结果的变量) 存在,返回 true,且将值赋值给参数2 若果键不存在,那么返回值为 false,且不为参数2赋值
getParamCached键,存储结果的变量)--提高变量获取效率 存在,返回 true,且将值赋值给参数2 若果键不存在,那么返回值为 false,且不为参数2赋值
getParamNames(std::vector<std::string>) 获取所有的键,并存储在参数 vector 中
hasParam(键) 是否包含某个键,存在返回 true,否则返回 false
searchParam(参数1,参数2) 搜索键,参数1是被搜索的键,参数2存储搜索结果的变量
ros::param ----- 与 NodeHandle 类似
*/
#include "ros/ros.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"get_param");
//NodeHandle-------------------------------------------------------- /* ros::NodeHandle nh; // param 函数 int res1 = nh.param("nh_int",100); // 键存在 int res2 = nh.param("nh_int2",100); // 键不存在 ROS_INFO("param获取结果:%d,%d",res1,res2);
// getParam 函数 int nh_int_value; double nh_double_value; bool nh_bool_value; std::string nh_string_value; std::vector<std::string> stus; std::map<std::string, std::string> friends;
nh.getParam("nh_int",nh_int_value); nh.getParam("nh_double",nh_double_value); nh.getParam("nh_bool",nh_bool_value); nh.getParam("nh_string",nh_string_value); nh.getParam("nh_vector",stus); nh.getParam("nh_map",friends);
ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d", nh_int_value, nh_double_value, nh_string_value.c_str(), nh_bool_value ); for (auto &&stu : stus) { ROS_INFO("stus 元素:%s",stu.c_str()); }
for (auto &&f : friends) { ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str()); }
// getParamCached() nh.getParamCached("nh_int",nh_int_value); ROS_INFO("通过缓存获取数据:%d",nh_int_value);
//getParamNames() std::vector<std::string> param_names1; nh.getParamNames(param_names1); for (auto &&name : param_names1) { ROS_INFO("名称解析name = %s",name.c_str()); } ROS_INFO("----------------------------");
ROS_INFO("存在 nh_int 吗? %d",nh.hasParam("nh_int")); ROS_INFO("存在 nh_intttt 吗? %d",nh.hasParam("nh_intttt"));
std::string key; nh.searchParam("nh_int",key); ROS_INFO("搜索键:%s",key.c_str()); */ //param-------------------------------------------------------- ROS_INFO("++++++++++++++++++++++++++++++++++++++++"); int res3 = ros::param::param("param_int",20); //存在 int res4 = ros::param::param("param_int2",20); // 不存在返回默认 ROS_INFO("param获取结果:%d,%d",res3,res4);
// getParam 函数 int param_int_value; double param_double_value; bool param_bool_value; std::string param_string_value; std::vector<std::string> param_stus; std::map<std::string, std::string> param_friends;
ros::param::get("param_int",param_int_value); ros::param::get("param_double",param_double_value); ros::param::get("param_bool",param_bool_value); ros::param::get("param_string",param_string_value); ros::param::get("param_vector",param_stus); ros::param::get("param_map",param_friends);
ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d", param_int_value, param_double_value, param_string_value.c_str(), param_bool_value ); for (auto &&stu : param_stus) { ROS_INFO("stus 元素:%s",stu.c_str()); }
for (auto &&f : param_friends) { ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str()); }
// getParamCached() ros::param::getCached("param_int",param_int_value); ROS_INFO("通过缓存获取数据:%d",param_int_value);
//getParamNames() std::vector<std::string> param_names2; ros::param::getParamNames(param_names2); for (auto &&name : param_names2) { ROS_INFO("名称解析name = %s",name.c_str()); } ROS_INFO("----------------------------");
ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int")); ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));
std::string key; ros::param::search("param_int",key); ROS_INFO("搜索键:%s",key.c_str());
return 0; } |
3.参数服务器删除参数
C++ /* 参数服务器操作之删除_C++实现:
ros::NodeHandle deleteParam("键") 根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
ros::param del("键") 根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false */ #include "ros/ros.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"delete_param");
ros::NodeHandle nh; bool r1 = nh.deleteParam("nh_int"); ROS_INFO("nh 删除结果:%d",r1);
bool r2 = ros::param::del("param_int"); ROS_INFO("param 删除结果:%d",r2);
return 0; } |
2.4 常用命令
机器人系统中启动的节点少则几个,多则十几个、几十个,不同的节点名称各异,通信时使用话题、服务、消息、参数等等都各不相同,一个显而易见的问题是: 当需要自定义节点和其他某个已经存在的节点通信时,如何获取对方的话题、以及消息载体的格式呢?
在 ROS 同提供了一些实用的命令行工具,可以用于获取不同节点的各类信息,常用的命令如下:
作用
和之前介绍的文件系统操作命令比较,文件操作命令是静态的,操作的是磁盘上的文件,而上述命令是动态的,在ROS程序启动后,可以动态的获取运行中的节点或参数的相关信息。
2.4.1 rosnode
rosnode 是用于获取节点信息的命令
Plaintext rosnode ping 测试节点的连接状态 rosnode list 列出活动节点 rosnode info 打印节点信息 rosnode machine 列出指定设备上节点 rosnode kill 杀死某个节点 rosnode cleanup 清除不可连接的节点 |
- 清除无用节点,启动乌龟节点,然后 ctrl + c 关闭,该节点并没被彻底清除,可以使用 cleanup 清除节点
2.4.2 rostopic
rostopic包含rostopic命令行工具,用于显示有关ROS 主题的调试信息,包括发布者,订阅者,发布频率和ROS消息。它还包含一个实验性Python库,用于动态获取有关主题的信息并与之交互。
Plaintext rostopic bw 显示主题使用的带宽 rostopic delay 显示带有 header 的主题延迟 rostopic echo 打印消息到屏幕 rostopic find 根据类型查找主题 rostopic hz 显示主题的发布频率 rostopic info 显示主题相关信息 rostopic list 显示所有活动状态下的主题 rostopic pub 将数据发布到主题 rostopic type 打印主题类型 |
- 直接调用即可,控制台将打印当前运行状态下的主题名称
- rostopic list -v : 获取话题详情(比如列出:发布者和订阅者个数...)
- 为roboware 自动生成的 发布/订阅 模型案例中的 订阅者 发布一条字符串
Plaintext rostopic pub /主题名称 消息类型 消息内容 rostopic pub /chatter std_msgs gagaxixi |
Plaintext rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear: x: 1.0 y: 0.0 z: 0.0 angular: x: 0.0 y: 0.0 z: 2.0" //只发布一次运动信息 rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist "linear: x: 1.0 y: 0.0 z: 0.0 angular: x: 0.0 y: 0.0 z: 2.0" // 以 10HZ 的频率循环发送运动信息 |
2.4.3 rosmsg
rosmsg是用于显示有关 ROS消息类型的 信息的命令行工具。
rosmsg 演示
Plaintext rosmsg show 显示消息描述 rosmsg info 显示消息信息 rosmsg list 列出所有消息 rosmsg md5 显示 md5 加密后的消息 rosmsg package 显示某个功能包下的所有消息 rosmsg packages 列出包含消息的功能包 |
Plaintext //rosmsg package 包名 rosmsg package turtlesim |
Plaintext //rosmsg show 消息名称 rosmsg show turtlesim/Pose 结果: float32 x float32 y float32 theta float32 linear_velocity float32 angular_velocity |
2.4.4 rosservice
rosservice包含用于列出和查询ROSServices的rosservice命令行工具。
调用部分服务时,如果对相关工作空间没有配置 path,需要进入工作空间调用 source ./devel/setup.bash
Plaintext rosservice args 打印服务参数 rosservice call 使用提供的参数调用服务 rosservice find 按照服务类型查找服务 rosservice info 打印有关服务的信息 rosservice list 列出所有活动的服务 rosservice type 打印服务类型 rosservice uri 打印服务的 ROSRPC uri |
Plaintext ~ rosservice list /clear /kill /listener/get_loggers /listener/set_logger_level /reset /rosout/get_loggers /rosout/set_logger_level /rostopic_4985_1578723066421/get_loggers /rostopic_4985_1578723066421/set_logger_level /rostopic_5582_1578724343069/get_loggers /rostopic_5582_1578724343069/set_logger_level /spawn /turtle1/set_pen /turtle1/teleport_absolute /turtle1/teleport_relative /turtlesim/get_loggers /turtlesim/set_logger_level |
Plaintext rosservice args /spawn x y theta name |
Plaintext rosservice call /spawn "x: 1.0 y: 2.0 theta: 0.0 name: 'xxx'" name: "xxx" //生成一只叫 xxx 的乌龟 |
2.4.5 rossrv
rossrv是用于显示有关ROS服务类型的信息的命令行工具,与 rosmsg 使用语法高度雷同。
Plaintext rossrv show 显示服务消息详情 rossrv info 显示服务消息相关信息 rossrv list 列出所有服务信息 rossrv md5 显示 md5 加密后的服务消息 rossrv package 显示某个包下所有服务消息 rossrv packages 显示包含服务消息的所有包 |
Plaintext //rossrv package 包名 rossrv package turtlesim |
- 对 service 数据使用 md5 校验(加密)
2.4.6 rosparam
rosparam包含rosparam命令行工具,用于使用YAML编码文件在参数服务器上获取和设置ROS参数。
Plaintext rosparam set 设置参数 rosparam get 获取参数 rosparam load 从外部文件加载参数 rosparam dump 将参数写出到外部文件 rosparam delete 删除参数 rosparam list 列出所有参数 |
Plaintext rosparam list //默认结果 /rosdistro /roslaunch/uris/host_helloros_virtual_machine__42911 /rosversion /run_id |
Plaintext rosparam set name huluwa //再次调用 rosparam list 结果 /name /rosdistro /roslaunch/uris/host_helloros_virtual_machine__42911 /rosversion /run_id |
Plaintext rosparam get name //结果 huluwa |
Plaintext rosparam delete name //结果 //去除了name |
- rosparam load(先准备 yaml 文件)
Plaintext rosparam load xxx.yaml |
Plaintext rosparam dump yyy.yaml |
2.5 通信机制实操
2.5.1 实操01_话题发布
需求描述:编码实现乌龟运动控制,让小乌龟做圆周运动。
结果演示:

实现分析:
- 乌龟运动控制实现,关键节点有两个,一个是乌龟运动显示节点 turtlesim_node,另一个是控制节点,二者是订阅发布模式实现通信的,乌龟运动显示节点直接调用即可,运动控制节点之前是使用的 turtle_teleop_key通过键盘 控制,现在需要自定义控制节点。
- 控制节点自实现时,首先需要了解控制节点与显示节点通信使用的话题与消息,可以使用ros命令结合计算图来获取。
- 了解了话题与消息之后,通过 C++ 或 Python 编写运动控制节点,通过指定的话题,按照一定的逻辑发布消息即可。
实现流程:
- 通过计算图结合ros命令获取话题与消息信息。
- 编码实现运动控制节点。
- 启动 roscore、turtlesim_node 以及自定义的控制节点,查看运行结果。
1.话题与消息获取
准备: 先启动键盘控制乌龟运动案例。
1.1话题获取
获取话题:/turtle1/cmd_vel
通过计算图查看话题,启动计算图:
或者通过 rostopic 列出话题:
1.2消息获取
获取消息类型:geometry_msgs/Twist
Plaintext rostopic type /turtle1/cmd_vel |
获取消息格式:
Plaintext rosmsg info geometry_msgs/Twist |
响应结果:
Plaintext geometry_msgs/Vector3 linear float64 x float64 y float64 z geometry_msgs/Vector3 angular float64 x float64 y float64 z |
linear(线速度) 下的xyz分别对应在x、y和z方向上的速度(单位是 m/s); angular(角速度)下的xyz分别对应x轴上的翻滚、y轴上俯仰和z轴上偏航的速度(单位是rad/s)。 详情请查看补充资料。 |
2.实现发布节点
创建功能包需要依赖的功能包: roscpp rospy std_msgs geometry_msgs
实现: C++
C++ /* 编写 ROS 节点,控制小乌龟画圆
准备工作: 1.获取topic(已知: /turtle1/cmd_vel) 2.获取消息类型(已知: geometry_msgs/Twist) 3.运行前,注意先启动 turtlesim_node 节点
实现流程: 1.包含头文件 2.初始化 ROS 节点 3.创建发布者对象 4.循环发布运动控制消息 */
#include "ros/ros.h" #include "geometry_msgs/Twist.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"control"); ros::NodeHandle nh; // 3.创建发布者对象 ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel",1000); // 4.循环发布运动控制消息 //4-1.组织消息 geometry_msgs::Twist msg; msg.linear.x = 1.0; msg.linear.y = 0.0; msg.linear.z = 0.0;
msg.angular.x = 0.0; msg.angular.y = 0.0; msg.angular.z = 2.0;
//4-2.设置发送频率 ros::Rate r(10); //4-3.循环发送 while (ros::ok()) { pub.publish(msg);
ros::spinOnce(); }
return 0; } |
配置文件此处略
权限设置以及配置文件此处略
3.运行
首先,启动 roscore;
然后启动乌龟显示节点;
最后执行运动控制节点;
最终执行结果与演示结果类似。
补充资料1:
弧度: 单位弧度定义为圆弧长度等于半径时的圆心角。

补充资料2:偏航、翻滚与俯仰
坐标系图解:

偏航:

俯仰:

翻滚:

2.5.2 实操02_话题订阅
需求描述: 已知turtlesim中的乌龟显示节点,会发布当前乌龟的位姿(窗体中乌龟的坐标以及朝向),要求控制乌龟运动,并时时打印当前乌龟的位姿。
结果演示:

实现分析:
- 首先,需要启动乌龟显示以及运动控制节点并控制乌龟运动。
- 要通过ROS命令,来获取乌龟位姿发布的话题以及消息。
- 编写订阅节点,订阅并打印乌龟的位姿。
实现流程:
- 通过ros命令获取话题与消息信息。
- 编码实现位姿获取节点。
- 启动 roscore、turtlesim_node 、控制节点以及位姿订阅节点,控制乌龟运动并输出乌龟的位姿。
1.话题与消息获取
获取话题:/turtle1/pose
获取消息类型:turtlesim/Pose
Plaintext rostopic type /turtle1/pose |
获取消息格式:
Plaintext rosmsg info turtlesim/Pose |
响应结果:
Plaintext float32 x float32 y float32 theta float32 linear_velocity float32 angular_velocity |
2.实现订阅节点
创建功能包需要依赖的功能包: roscpp rospy std_msgs turtlesim
实现: C++
C++ /* 订阅小乌龟的位姿: 时时获取小乌龟在窗体中的坐标并打印 准备工作: 1.获取话题名称 /turtle1/pose 2.获取消息类型 turtlesim/Pose 3.运行前启动 turtlesim_node 与 turtle_teleop_key 节点
实现流程: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建订阅者对象 5.回调函数处理订阅的数据 6.spin */
#include "ros/ros.h" #include "turtlesim/Pose.h"
void doPose(const turtlesim::Pose::ConstPtr& p) { ROS_INFO("乌龟位姿信息:x=%.2f,y=%.2f,theta=%.2f,lv=%.2f,av=%.2f", p->x,p->y,p->theta,p->linear_velocity,p->angular_velocity ); }
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"sub_pose"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建订阅者对象 ros::Subscriber sub = nh.subscribe<turtlesim::Pose>("/turtle1/pose",1000,doPose); // 5.回调函数处理订阅的数据 // 6.spin ros::spin(); return 0; } |
配置文件此处略
3.运行
首先,启动 roscore;
然后启动乌龟显示节点,执行运动控制节点;
最后启动乌龟位姿订阅节点;
最终执行结果与演示结果类似。
2.5.3 实操03_服务调用
需求描述:编码实现向 turtlesim 发送请求,在乌龟显示节点的窗体指定位置生成一乌龟,这是一个服务请求操作。
结果演示:

实现分析:
- 首先,需要启动乌龟显示节点。
- 要通过ROS命令,来获取乌龟生成服务的服务名称以及服务消息类型。
- 编写服务请求节点,生成新的乌龟。
实现流程:
- 通过ros命令获取服务与服务消息信息。
- 编码实现服务请求节点。
- 启动 roscore、turtlesim_node 、乌龟生成节点,生成新的乌龟。
1.服务名称与服务消息获取
获取话题:/spawn
获取消息类型:turtlesim/Spawn
Plaintext rosservice type /spawn |
获取消息格式:
Plaintext rossrv info turtlesim/Spawn |
响应结果:
C++ float32 x float32 y float32 theta string name --- string name |
2.服务客户端实现
创建功能包需要依赖的功能包: roscpp rospy std_msgs turtlesim
实现:C++
C++ /* 生成一只小乌龟 准备工作: 1.服务话题 /spawn 2.服务消息类型 turtlesim/Spawn 3.运行前先启动 turtlesim_node 节点
实现流程: 1.包含头文件 需要包含 turtlesim 包下资源,注意在 package.xml 配置 2.初始化 ros 节点 3.创建 ros 句柄 4.创建 service 客户端 5.等待服务启动 6.发送请求 7.处理响应
*/
#include "ros/ros.h" #include "turtlesim/Spawn.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ros 节点 ros::init(argc,argv,"set_turtle"); // 3.创建 ros 句柄 ros::NodeHandle nh; // 4.创建 service 客户端 ros::ServiceClient client = nh.serviceClient<turtlesim::Spawn>("/spawn"); // 5.等待服务启动 // client.waitForExistence(); ros::service::waitForService("/spawn"); // 6.发送请求 turtlesim::Spawn spawn; spawn.request.x = 1.0; spawn.request.y = 1.0; spawn.request.theta = 1.57; spawn.request.name = "my_turtle"; bool flag = client.call(spawn); // 7.处理响应结果 if (flag) { ROS_INFO("新的乌龟生成,名字:%s",spawn.response.name.c_str()); } else { ROS_INFO("乌龟生成失败!!!"); }
return 0; } |
配置文件此处略
3.运行
首先,启动 roscore;
然后启动乌龟显示节点;
最后启动乌龟生成请求节点;
最终执行结果与演示结果类似。
2.5.4 实操04_参数设置
需求描述: 修改turtlesim乌龟显示节点窗体的背景色,已知背景色是通过参数服务器的方式以 rgb 方式设置的。
结果演示:

实现分析:
- 首先,需要启动乌龟显示节点。
- 要通过ROS命令,来获取参数服务器中设置背景色的参数。
- 编写参数设置节点,修改参数服务器中的参数值。
实现流程:
- 通过ros命令获取参数。
- 编码实现服参数设置节点。
- 启动 roscore、turtlesim_node 与参数设置节点,查看运行结果。
1.参数名获取
获取参数列表:
响应结果:
Plaintext /turtlesim/background_b /turtlesim/background_g /turtlesim/background_r |
2.参数修改
实现:C++
C++ /* 注意命名空间的使用。
*/ #include "ros/ros.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"haha");
ros::NodeHandle nh("turtlesim"); //ros::NodeHandle nh;
// ros::param::set("/turtlesim/background_r",0); // ros::param::set("/turtlesim/background_g",0); // ros::param::set("/turtlesim/background_b",0);
nh.setParam("background_r",0); nh.setParam("background_g",0); nh.setParam("background_b",0);
return 0; } |
配置文件此处略
3.运行
首先,启动 roscore;
然后启动背景色设置节点;
最后启动乌龟显示节点;
最终执行结果与演示结果类似。
PS: 注意节点启动顺序,如果先启动乌龟显示节点,后启动背景色设置节点,那么颜色设置不会生效。
4.其他设置方式
方式1:修改小乌龟节点的背景色(命令行实现)
Plaintext rosparam set /turtlesim/background_b 自定义数值 |
Plaintext rosparam set /turtlesim/background_g 自定义数值 |
Plaintext rosparam set /turtlesim/background_r 自定义数值 |
修改相关参数后,重启 turtlesim_node 节点,背景色就会发生改变了
方式2:启动节点时,直接设置参数
Plaintext rosrun turtlesim turtlesim_node _background_r:=100 _background_g=0 _background_b=0 |
方式3:通过launch文件传参
Plaintext <launch> <node pkg="turtlesim" type="turtlesim_node" name="set_bg" output="screen"> <!-- launch 传参策略1 --> <!-- <param name="background_b" value="0" type="int" /> <param name="background_g" value="0" type="int" /> <param name="background_r" value="0" type="int" /> -->
<!-- launch 传参策略2 --> <rosparam command="load" file="$(find demo03_test_parameter)/cfg/color.yaml" /> </node>
</ |
2.6 通信机制比较
三种通信机制中,参数服务器是一种数据共享机制,可以在不同的节点之间共享数据,话题通信与服务通信是在不同的节点之间传递数据的,三者是ROS中最基础也是应用最为广泛的通信机制。
这其中,话题通信和服务通信有一定的相似性也有本质上的差异,在此将二者做一下简单比较:
二者的实现流程是比较相似的,都是涉及到四个要素:
- 要素1: 消息的发布方/客户端(Publisher/Client)
- 要素2: 消息的订阅方/服务端(Subscriber/Server)
可以概括为: 两个节点通过话题关联到一起,并使用某种类型的数据载体实现数据传输。
二者的实现也是有本质差异的,具体比较如下:

点击图片可查看完整电子表格
不同通信机制有一定的互补性,都有各自适应的应用场景。尤其是话题与服务通信,需要结合具体的应用场景与二者的差异,选择合适的通信机制。
2.7 动作通讯
关于action通信,我们先从之前导航中的应用场景开始介绍,描述如下: 机器人导航到某个目标点,此过程需要一个节点A发布目标信息,然后一个节点B接收到请求并控制移动,最终响应目标达成状态信息。 |
乍一看,这好像是服务通信实现,因为需求中要A发送目标,B执行并返回结果,这是一个典型的基于请求响应的应答模式,不过,如果只是使用基本的服务通信实现,存在一个问题:导航是一个过程,是耗时操作,如果使用服务通信,那么只有在导航结束时,才会产生响应结果,而在导航过程中,节点A是不会获取到任何反馈的,从而可能出现程序"假死"的现象,过程的不可控意味着不良的用户体验,以及逻辑处理的缺陷(比如:导航中止的需求无法实现)。更合理的方案应该是:导航过程中,可以连续反馈当前机器人状态信息,当导航终止时,再返回最终的执行结果。在ROS中,该实现策略称之为:action 通信。
概念
在ROS中提供了actionlib功能包集,用于实现 action 通信。action 是一种类似于服务通信的实现,其实现模型也包含请求和响应,但是不同的是,在请求和响应的过程中,服务端还可以连续的反馈当前任务进度,客户端可以接收连续反馈并且还可以取消任务。
action结构图解:

action通信接口图解:

作用
一般适用于耗时的请求响应场景,用以获取连续的状态反馈。
案例
创建两个ROS 节点,服务器和客户端,客户端可以向服务器发送目标数据N(一个整型数据)服务器会计算 1 到 N 之间所有整数的和,这是一个循环累加的过程,返回给客户端,这是基于请求响应模式的,又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s,为了良好的用户体验,需要服务器在计算过程中,每累加一次,就给客户端响应一次百分比格式的执行进度,使用 action实现。

另请参考:
2.7.1 action通信自定义action文件
action、srv、msg 文件内的可用数据类型一致,且三者实现流程类似:
- 按照固定格式创建action文件;
- 编辑配置文件;
- 编译生成中间文件。
1.定义action文件
首先新建功能包,并导入依赖: roscpp rospy std_msgs actionlib actionlib_msgs;
然后功能包下新建 action 目录,新增 Xxx.action(比如:AddInts.action)。
C++ #目标值 int32 num --- #最终结果 int32 result --- #连续反馈 float64 progress_bar |
2.编辑配置文件
CMakeLists.txt
CMake find_package (catkin REQUIRED COMPONENTS roscpp rospy std_msgs actionlib actionlib_msgs ) |
C++ add_action_files( FILES AddInts.action ) |
CMake generate_messages( DEPENDENCIES std_msgs actionlib_msgs ) |
CMake catkin_package( INCLUDE_DIRS include LIBRARIES demo04_action CATKIN_DEPENDS roscpp rospy std_msgs actionlib actionlib_msgs DEPENDS system_lib ) |
3.编译
编译后会生成一些中间文件。
msg文件(.../工作空间/devel/share/包名/msg/xxx.msg):

C++ 调用的文件(.../工作空间/devel/include/包名/xxx.h):

2.7.2 action通信自定义action文件调用A(C++)
需求:
创建两个ROS 节点,服务器和客户端,客户端可以向服务器发送目标数据N(一个整型数据)服务器会计算 1 到 N 之间所有整数的和,这是一个循环累加的过程,返回给客户端,这是基于请求响应模式的,又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s,为了良好的用户体验,需要服务器在计算过程中,每累加一次,就给客户端响应一次百分比格式的执行进度,使用 action实现。 |
流程:
- 编写action服务端实现;
- 编写action客户端实现;
- 编辑CMakeLists.txt;
- 编译并执行。
0.vscode配置
需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:
Plaintext { "configurations": [ { "browse": { "databaseFilename": "", "limitSymbolsToIncludedHeaders": true }, "includePath": [ "/opt/ros/noetic/include/**", "/usr/include/**", "/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径 ], "name": "ROS", "intelliSenseMode": "gcc-x64", "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17" } ], "version": 4 } |
1.服务端
C++ #include "ros/ros.h" #include "actionlib/server/simple_action_server.h" #include "demo01_action/AddIntsAction.h" /* 需求: 创建两个ROS节点,服务器和客户端, 客户端可以向服务器发送目标数据N(一个整型数据) 服务器会计算1到N之间所有整数的和,这是一个循环累加的过程,返回给客户端, 这是基于请求响应模式的, 又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s, 为了良好的用户体验,需要服务器在计算过程中, 每累加一次,就给客户端响应一次百分比格式的执行进度,使用action实现。
流程: 1.包含头文件; 2.初始化ROS节点; 3.创建NodeHandle; 4.创建action服务对象; 5.处理请求,产生反馈与响应; 6.spin().
*/
typedef actionlib::SimpleActionServer<demo01_action::AddIntsAction> Server;
void cb(const demo01_action::AddIntsGoalConstPtr &goal,Server* server){ //获取目标值 int num = goal->num; ROS_INFO("目标值:%d",num); //累加并响应连续反馈 int result = 0; demo01_action::AddIntsFeedback feedback;//连续反馈 ros::Rate rate(10);//通过频率设置休眠时间 for (int i = 1; i <= num; i++) { result += i; //组织连续数据并发布 feedback.progress_bar = i / (double)num; server->publishFeedback(feedback); rate.sleep(); } //设置最终结果 demo01_action::AddIntsResult r; r.result = result; server->setSucceeded(r); ROS_INFO("最终结果:%d",r.result); }
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ROS_INFO("action服务端实现"); // 2.初始化ROS节点; ros::init(argc,argv,"AddInts_server"); // 3.创建NodeHandle; ros::NodeHandle nh; // 4.创建action服务对象; /*SimpleActionServer(ros::NodeHandle n, std::string name, boost::function<void (const demo01_action::AddIntsGoalConstPtr &)> execute_callback, bool auto_start) */ // actionlib::SimpleActionServer<demo01_action::AddIntsAction> server(....); Server server(nh,"addInts",boost::bind(&cb,_1,&server),false); server.start(); // 5.处理请求,产生反馈与响应;
// 6.spin(). ros::spin(); return 0; } |
PS:
可以先配置CMakeLists.tx文件并启动上述action服务端,然后通过 rostopic 查看话题,向action相关话题发送消息,或订阅action相关话题的消息。
2.客户端
C++ #include "ros/ros.h" #include "actionlib/client/simple_action_client.h" #include "demo01_action/AddIntsAction.h"
/* 需求: 创建两个ROS节点,服务器和客户端, 客户端可以向服务器发送目标数据N(一个整型数据) 服务器会计算1到N之间所有整数的和,这是一个循环累加的过程,返回给客户端, 这是基于请求响应模式的, 又已知服务器从接收到请求到产生响应是一个耗时操作,每累加一次耗时0.1s, 为了良好的用户体验,需要服务器在计算过程中, 每累加一次,就给客户端响应一次百分比格式的执行进度,使用action实现。
流程: 1.包含头文件; 2.初始化ROS节点; 3.创建NodeHandle; 4.创建action客户端对象; 5.发送目标,处理反馈以及最终结果; 6.spin().
*/ typedef actionlib::SimpleActionClient<demo01_action::AddIntsAction> Client;
//处理最终结果 void done_cb(const actionlib::SimpleClientGoalState &state, const demo01_action::AddIntsResultConstPtr &result){ if (state.state_ == state.SUCCEEDED) { ROS_INFO("最终结果:%d",result->result); } else { ROS_INFO("任务失败!"); }
} //服务已经激活 void active_cb(){ ROS_INFO("服务已经被激活...."); } //处理连续反馈 void feedback_cb(const demo01_action::AddIntsFeedbackConstPtr &feedback){ ROS_INFO("当前进度:%.2f",feedback->progress_bar); }
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化ROS节点; ros::init(argc,argv,"AddInts_client"); // 3.创建NodeHandle; ros::NodeHandle nh; // 4.创建action客户端对象; // SimpleActionClient(ros::NodeHandle & n, const std::string & name, bool spin_thread = true) // actionlib::SimpleActionClient<demo01_action::AddIntsAction> client(nh,"addInts"); Client client(nh,"addInts",true); //等待服务启动 client.waitForServer(); // 5.发送目标,处理反馈以及最终结果; /* void sendGoal(const demo01_action::AddIntsGoal &goal, boost::function<void (const actionlib::SimpleClientGoalState &state, const demo01_action::AddIntsResultConstPtr &result)> done_cb, boost::function<void ()> active_cb, boost::function<void (const demo01_action::AddIntsFeedbackConstPtr &feedback)> feedback_cb)
*/ demo01_action::AddIntsGoal goal; goal.num = 10;
client.sendGoal(goal,&done_cb,&active_cb,&feedback_cb); // 6.spin(). ros::spin(); return 0; } |
PS:等待服务启动,只可以使用client.waitForServer();,之前服务中等待启动的另一种方式ros::service::waitForService("addInts");不适用
3.编译配置文件
Plaintext add_executable(action01_server src/action01_server.cpp) add_executable(action02_client src/action02_client.cpp) ... add_dependencies(action01_server ${$ {PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) add_dependencies(action02_client ${$ {PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) ... target_link_libraries(action01_server ${catkin_LIBRARIES} ) target_link_libraries(action02_client ${catkin_LIBRARIES} ) |
4.执行
首先启动 roscore,然后分别启动action服务端与action客户端,最终运行结果与案例类似。
2.7.3 action无用的项目
客户端
C++ #include "ros/ros.h" #include "actionlib/client/simple_action_client.h" #include "work/workAction.h" /* 需求: 画一个五边形并实时反馈结果 大致思路: 每到暂停处通过一个对象记录对当前位置,通过该对象与乌龟实时坐标进行距离运算,直到乌龟到达距离,乌龟停下,换挡 进行旋转75度,循环5次操作,通过某一变量进行计数,当达到5次时,进行return
流程: 1.包含头文件; 2.初始化ROS节点; 3.创建NodeHandle; 4.创建action客服端对象; 5.发送请求: a,建立联系-----回调函数 b,处理连续反馈---回调函数 c,处理最终响应 ---回调函数 6.spin().
*/ // 响应成功时的回调 void succeed(const actionlib::SimpleClientGoalState &state, const work::workResultConstPtr &result) { //响应状态是否是成功 if(state.state_ == state.SUCCEEDED) { ROS_INFO("画图成功响应成功"); } else { ROS_INFO("画图失败"); } } //激活的回调 void active() { ROS_INFO("链接成功建立..."); } //连续反馈的回调 void feed(const work::workFeedbackConstPtr &feedback) { ROS_INFO("当前进度%.2f",feedback->progress); } int main(int argc, char *argv[]) {
// 2.初始化ROS节点; setlocale(LC_ALL,""); ros::init(argc,argv,"turtlesim_client"); // 3.创建NodeHandle; ros::NodeHandle nh; // 4.创建action客服端对象; actionlib::SimpleActionClient<work::workAction> client(nh,"turtle_control"); // 5.发送请求: // 注意:先判断服务端状态 ROS_INFO("等待服务器启动"); // void sendGoal(const demon01_action::addintsGoal &goal, // boost::function<void (const actionlib::SimpleClientGoalState &state, const demon01_action::addintsResultConstPtr &result)> done_cb, // boost::function<void ()> active_cb, // boost::function<void (const demon01_action::addintsFeedbackConstPtr &feedback)> feedback_cb) // a,建立联系-----回调函数 // b,处理连续反馈---回调函数 // c,处理最终响应 ---回调函数 //参数1:设置目标值 work::workGoal set; set.num = 100; client.sendGoal(set,&succeed,&active,&feed); // 6.spin(). ros::spin(); return 0; } |
客户端
C++ #include "ros/ros.h" #include "actionlib/server/simple_action_server.h" #include "work/workAction.h" #include "geometry_msgs/Twist.h" typedef actionlib::SimpleActionServer<work::workAction> Server;
void turtles_recept(const work::workGoalConstPtr &goal,Server* server) { ros::NodeHandle nh; ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("turtle1/cmd_vel",10); geometry_msgs::Twist v; v.linear.x = 1; v.linear.y = 0; v.linear.z = 0; v.angular.x = 0; v.angular.y = 0; v.angular.z = 0; double a,b; ros::Rate rate(10); int t = 0; double n = 0; while(ros::ok()) { v.linear.x = 1; v.angular.z = 0; a = ros::Time::now().toSec(); while ((b-a)*v.linear.x<=2) { pub.publish(v); b = ros::Time::now().toSec(); n++; work::workFeedback fb; fb.progress = n/(double)goal->num; server->publishFeedback(fb); rate.sleep(); } v.linear.x = 0; v.angular.z = 1; a = ros::Time::now().toSec(); while ((b-a)*v.angular.z<=1.16) { pub.publish(v); b = ros::Time::now().toSec(); rate.sleep(); } t++; if(t==5) { break; } }
} int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"work_server"); ros::NodeHandle nh; Server server(nh,"turtle_control",boost::bind(&turtles_recept,_1,&server),false); server.start(); ROS_INFO("服务启动成功"); ros::spin(); return 0; } |
第 3 章 一些API和操作
3.1 常用API
首先,建议参考官方API文档或参考源码:
参数服务器相关API在第二章已经有详细介绍和应用,在此不再赘述。
另请参考:
3.1.1 初始化
C++
初始化
Plaintext /** @brief ROS初始化函数。 * 该函数可以解析并使用节点启动时传入的参数(通过参数设置节点名称、命名空间...) * 该函数有多个重载版本,如果使用NodeHandle建议调用该版本。 * \param argc 参数个数 \param argv 参数列表 \param name 节点名称,需要保证其唯一性,不允许包含命名空间 \param options 节点启动选项,被封装进了ros::init_options * */void init(int &argc, char **argv, const std::string& name, uint32_t options = 0); |
3.1.2 话题与服务相关对象
C++
在 roscpp 中,话题和服务的相关对象一般由 NodeHandle 创建。
NodeHandle有一个重要作用是可以用于设置命名空间,这是后期的重点,但是本章暂不介绍。
1.发布对象
对象获取:
C++ /** * \brief 根据话题生成发布对象 * * 在 ROS master 注册并返回一个发布者对象,该对象可以发布消息 * * 使用示例如下: * * ros::Publisher pub = handle.advertise<std_msgs::Empty>("my_topic", 1); * * \param topic 发布消息使用的话题 * * \param queue_size 等待发送给订阅者的最大消息数量 * * \param latch (optional) 如果为 true,该话题发布的最后一条消息将被保存,并且后期当有订阅者连接时会将该消息发送给订阅者 * * \return 调用成功时,会返回一个发布对象 * * */ template <class M> Publisher advertise(const std::string& topic, uint32_t queue_size, bool latch = false) |
消息发布函数:
C++ /** * 发布消息 */ template <typename M> void publish(const M& message) const |
2.订阅对象
对象获取:
C++ /** * \brief 生成某个话题的订阅对象 * * 该函数将根据给定的话题在ROS master 注册,并自动连接相同主题的发布方,每接收到一条消息,都会调用回调 * 函数,并且传入该消息的共享指针,该消息不能被修改,因为可能其他订阅对象也会使用该消息。 * * 使用示例如下:
void callback(const std_msgs::Empty::ConstPtr& message) { }
ros::Subscriber sub = handle.subscribe("my_topic", 1, callback);
* * \param M [template] M 是指消息类型 * \param topic 订阅的话题 * \param queue_size 消息队列长度,超出长度时,头部的消息将被弃用 * \param fp 当订阅到一条消息时,需要执行的回调函数 * \return 调用成功时,返回一个订阅者对象,失败时,返回空对象 *
void callback(const std_msgs::Empty::ConstPtr& message){...} ros::NodeHandle nodeHandle; ros::Subscriber sub = nodeHandle.subscribe("my_topic", 1, callback); if (sub) // Enter if subscriber is valid { ... }
*/ template<class M> Subscriber subscribe(const std::string& topic, uint32_t queue_size, void(*fp)(const boost::shared_ptr<M const>&), const TransportHints& transport_hints = TransportHints()) |
3.服务对象
对象获取:
Python /** * \brief 生成服务端对象 * * 该函数可以连接到 ROS master,并提供一个具有给定名称的服务对象。 * * 使用示例如下: \verbatim bool callback(std_srvs::Empty& request, std_srvs::Empty& response) { return true; }
ros::ServiceServer service = handle.advertiseService("my_service", callback); \endverbatim * * \param service 服务的主题名称 * \param srv_func 接收到请求时,需要处理请求的回调函数 * \return 请求成功时返回服务对象,否则返回空对象: \verbatim bool Foo::callback(std_srvs::Empty& request, std_srvs::Empty& response) { return true; } ros::NodeHandle nodeHandle; Foo foo_object; ros::ServiceServer service = nodeHandle.advertiseService("my_service", callback); if (service) // Enter if advertised service is valid { ... } \endverbatim
*/ template<class MReq, class MRes> ServiceServer advertiseService(const std::string& service, bool(*srv_func)(MReq&, MRes&)) |
4.客户端对象
对象获取:
C++ /** * @brief 创建一个服务客户端对象 * * 当清除最后一个连接的引用句柄时,连接将被关闭。 * * @param service_name 服务主题名称 */ template<class Service> ServiceClient serviceClient(const std::string& service_name, bool persistent = false, const M_string& header_values = M_string()) |
请求发送函数:
C++ /** * @brief 发送请求 * 返回值为 bool 类型,true,请求处理成功,false,处理失败。 */ template<class Service> bool call(Service& service) |
等待服务函数1:
C++ /** * ros::service::waitForService("addInts"); * \brief 等待服务可用,否则一致处于阻塞状态 * \param service_name 被"等待"的服务的话题名称 * \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭 * \return 成功返回 true,否则返回 false。 */ ROSCPP_DECL bool waitForService(const std::string& service_name, ros::Duration timeout = ros::Duration(-1)) |
等待服务函数2:
C++ /** * client.waitForExistence(); * \brief 等待服务可用,否则一致处于阻塞状态 * \param timeout 等待最大时常,默认为 -1,可以永久等待直至节点关闭 * \return 成功返回 true,否则返回 false。 */ bool waitForExistence(ros::Duration timeout = ros::Duration(-1)); |
3.1.3 回旋函数
C++
在ROS程序中,频繁的使用了 ros::spin() 和 ros::spinOnce() 两个回旋函数,可以用于处理回调函数。
1.spinOnce()
C++ /** * \brief 处理一轮回调 * * 一般应用场景: * 在循环体内,处理所有可用的回调函数 * */ ROSCPP_DECL void spinOnce(); |
2.spin()
C++ /** * \brief 进入循环处理回调 */ ROSCPP_DECL void spin(); |
3.二者比较
相同点:二者都用于处理回调函数;
不同点:ros::spin() 是进入了循环执行回调函数,而 ros::spinOnce() 只会执行一次回调函数(没有循环),在 ros::spin() 后的语句不会执行到,而 ros::spinOnce() 后的语句可以执行。
3.1.4 时间
ROS中时间相关的API是极其常用,比如:获取当前时刻、持续时间的设置、执行频率、休眠、定时器...都与时间相关。
C++
1.时刻
获取时刻,或是设置指定时刻:
C++ ros::init(argc,argv,"hello_time"); ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败 ros::Time right_now = ros::Time::now();//将当前时刻封装成对象 ROS_INFO("当前时刻:%.2f",right_now.toSec());//获取距离 1970年01月01日 00:00:00 的秒数 ROS_INFO("当前时刻:%d",right_now.sec);//获取距离 1970年01月01日 00:00:00 的秒数
ros::Time someTime(100,100000000);// 参数1:秒数 参数2:纳秒 ROS_INFO("时刻:%.2f",someTime.toSec()); //100.10 ros::Time someTime2(100.3);//直接传入 double 类型的秒数 ROS_INFO("时刻:%.2f",someTime2.toSec()); //100.30 |
2.持续时间
设置一个时间区间(间隔):
C++ ROS_INFO("当前时刻:%.2f",ros::Time::now().toSec()); ros::Duration du(10);//持续10秒钟,参数是double类型的,以秒为单位 du.sleep();//按照指定的持续时间休眠 ROS_INFO("持续时间:%.2f",du.toSec());//将持续时间换算成秒 ROS_INFO("当前时刻:%.2f",ros::Time::now().toSec()); |
3.持续时间与时刻运算
为了方便使用,ROS中提供了时间与时刻的运算:
C++ ROS_INFO("时间运算"); ros::Time now = ros::Time::now(); ros::Duration du1(10); ros::Duration du2(20); ROS_INFO("当前时刻:%.2f",now.toSec()); //1.time 与 duration 运算 ros::Time after_now = now + du1; ros::Time before_now = now - du1; ROS_INFO("当前时刻之后:%.2f",after_now.toSec()); ROS_INFO("当前时刻之前:%.2f",before_now.toSec());
//2.duration 之间相互运算 ros::Duration du3 = du1 + du2; ros::Duration du4 = du1 - du2; ROS_INFO("du3 = %.2f",du3.toSec()); ROS_INFO("du4 = %.2f",du4.toSec()); //PS: time 与 time 不可以运算 // ros::Time nn = now + before_now;//异常 |
4.设置运行频率
C++ ros::Rate rate(1);//指定频率 while (true) { ROS_INFO("-----------code----------"); rate.sleep();//休眠,休眠时间 = 1 / 频率。 } |
5.定时器
ROS 中内置了专门的定时器,可以实现与 ros::Rate 类似的效果:
C++ ros::NodeHandle nh;//必须创建句柄,否则时间没有初始化,导致后续API调用失败
// ROS 定时器 /** * \brief 创建一个定时器,按照指定频率调用回调函数。 * * \param period 时间间隔 * \param callback 回调函数 * \param oneshot 如果设置为 true,只执行一次回调函数,设置为 false,就循环执行。 * \param autostart 如果为true,返回已经启动的定时器,设置为 false,需要手动启动。 */ //Timer createTimer(Duration period, const TimerCallback& callback, bool oneshot = false, // bool autostart = true) const;
// ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing); ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,true);//只执行一次
// ros::Timer timer = nh.createTimer(ros::Duration(0.5),doSomeThing,false,false);//需要手动启动 // timer.start(); ros::spin(); //必须 spin |
定时器的回调函数:
C++ void doSomeThing(const ros::TimerEvent &event){ ROS_INFO("-------------"); ROS_INFO("event:%s",std::to_string(event.current_real.toSec()).c_str()); } |
3.1.5 其他函数
在发布实现时,一般会循环发布消息,循环的判断条件一般由节点状态来控制,C++中可以通过 ros::ok() 来判断节点状态是否正常,而 python 中则通过 rospy.is_shutdown() 来实现判断,导致节点退出的原因主要有如下几种:
- 节点接收到了关闭信息,比如常用的 ctrl + c 快捷键就是关闭节点的信号;
- 程序中的其他部分调用了节点关闭相关的API(C++中是ros::shutdown(),python中是rospy.signal_shutdown())
另外,日志相关的函数也是极其常用的,在ROS中日志被划分成如下级别:
- DEBUG(调试):只在调试时使用,此类消息不会输出到控制台;
- INFO(信息):标准消息,一般用于说明系统内正在执行的操作;
- WARN(警告):提醒一些异常情况,但程序仍然可以执行;
- ERROR(错误):提示错误信息,此类错误会影响程序运行;
- FATAL(严重错误):此类错误将阻止节点继续运行。
C++
1.节点状态判断
C++ /** \brief 检查节点是否已经退出 * * ros::shutdown() 被调用且执行完毕后,该函数将会返回 false * * \return true 如果节点还健在, false 如果节点已经火化了。 */ bool ok(); |
2.节点关闭函数
C++ /* * 关闭节点 */ void shutdown(); |
3.日志函数
使用示例
C++ ROS_DEBUG("hello,DEBUG"); //不会输出 ROS_INFO("hello,INFO"); //默认白色字体 ROS_WARN("Hello,WARN"); //默认黄色字体 ROS_ERROR("hello,ERROR");//默认红色字体 ROS_FATAL("hello,FATAL");//默认红色字体Copy |
3.2 ROS中的头文件和源文件
3.2.1 自定义头文件调用
需求:设计头文件,可执行文件本身作为源文件。
流程:
- 编写头文件;
- 编写可执行文件(同时也是源文件);
- 编辑配置文件并执行。
1.头文件
在功能包下的 include/功能包名 目录下新建头文件: hello.h,示例内容如下:
C++ #ifndef _HELLO_H #define _HELLO_H
namespace hello_ns { class HelloPub { public: void run(); }; }
#endif |
注意:
在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性
Plaintext "/home/用户/工作空间/src/功能包/include/**" |
2.可执行文件
在 src 目录下新建文件:hello.cpp,示例内容如下:
C++ #include "ros/ros.h" #include "test_head/hello.h" namespace hello_ns {
void HelloPub::run() { ROS_INFO("自定义头文件的使用...."); } }
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); ros::init(argc,argv,"test_head_node"); hello_ns::HelloPub helloPub; helloPub.run(); return 0; } |
3.配置文件
配置CMakeLists.txt文件,头文件相关配置如下:
CMake include_directories( include ${catkin_INCLUDE_DIRS} ) |
可执行配置文件配置方式与之前一致:
CMake add_executable(hello src/hello.cpp)
add_dependencies(hello ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(hello ${catkin_LIBRARIES} ) |
最后,编译并执行,控制台可以输出自定义的文本信息。
3.2.2 自定义源文件调用
需求:设计头文件与源文件,在可执行文件中包含头文件。
流程:
- 编写头文件;
- 编写源文件;
- 编写可执行文件;
- 编辑配置文件并执行。
1.头文件
头文件设置于 3.2.1 类似,在功能包下的 include/功能包名 目录下新建头文件: haha.h,示例内容如下:
C++ #ifndef _HAHA_H #define _HAHA_H
namespace hello_ns {
class My { public: void run(); };
}
#endif |
注意:
在 VScode 中,为了后续包含头文件时不抛出异常,请配置 .vscode 下 c_cpp_properties.json 的 includepath属性
Plaintext "/home/用户/工作空间/src/功能包/include/**" |
2.源文件
在 src 目录下新建文件:haha.cpp,示例内容如下:
C++ #include "test_head_src/haha.h" #include "ros/ros.h"
namespace hello_ns {
void My::run() { ROS_INFO("hello,head and src ..."); } } |
3.可执行文件
在 src 目录下新建文件: use_head.cpp,示例内容如下:
C++ #include "ros/ros.h" #include "test_head_src/haha.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"hahah"); hello_ns::My my; my.run(); return 0; } |
4.配置文件
头文件与源文件相关配置:
CMake include_directories( include ${catkin_INCLUDE_DIRS} )
## 声明C++库 add_library(head include/test_head_src/haha.h src/haha.cpp )
add_dependencies(head ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(head ${catkin_LIBRARIES} ) |
可执行文件配置:
C++ add_executable(use_head src/use_head.cpp)
add_dependencies(use_head ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
#此处需要添加之前设置的 head 库 target_link_libraries(use_head head ${catkin_LIBRARIES} ) |
第 4 章 ROS运行管理
ROS是多进程(节点)的分布式框架,一个完整的ROS系统实现: 可能包含多台主机; 每台主机上又有多个工作空间(workspace); 每个的工作空间中又包含多个功能包(package); 每个功能包又包含多个节点(Node),不同的节点都有自己的节点名称; 每个节点可能还会设置一个或多个话题(topic)... |

在多级层深的ROS系统中,其实现与维护可能会出现一些问题,比如,如何关联不同的功能包,繁多的ROS节点应该如何启动?功能包、节点、话题、参数重名时应该如何处理?不同主机上的节点如何通信?
本章主要内容介绍在ROS中上述问题的解决策略(见本章目录),预期达成学习目标也与上述问题对应:
- 理解什么是ROS工作空间覆盖,以及存在什么安全隐患;
4.1 ROS元功能包
场景:完成ROS中一个系统性的功能,可能涉及到多个功能包,比如实现了机器人导航模块,该模块下有地图、定位、路径规划...等不同的子级功能包。那么调用者安装该模块时,需要逐一的安装每一个功能包吗? |
显而易见的,逐一安装功能包的效率低下,在ROS中,提供了一种方式可以将不同的功能包打包成一个功能包,当安装某个功能模块时,直接调用打包后的功能包即可,该包又称之为元功能包(metapackage)。
概念
MetaPackage是Linux的一个文件管理系统的概念。是ROS中的一个虚包,里面没有实质性的内容,但是它依赖了其他的软件包,通过这种方法可以把其他包组合起来,我们可以认为它是一本书的目录索引,告诉我们这个包集合中有哪些子包,并且该去哪里下载。
例如:
- sudo apt install ros-noetic-desktop-full 命令安装ros时就使用了元功能包,该元功能包依赖于ROS中的其他一些功能包,安装该包时会一并安装依赖。
还有一些常见的MetaPackage:navigation moveit! turtlebot3 ....
作用
方便用户的安装,我们只需要这一个包就可以把其他相关的软件包组织到一起安装了。
实现
首先:新建一个功能包
然后:修改package.xml ,内容如下:
Plaintext <exec_depend>被集成的功能包</exec_depend> ..... <export> <metapackage /> </export> |
最后:修改 CMakeLists.txt,内容如下:
CMake cmake_minimum_required(VERSION 3.0.2) project(demo) find_package(catkin REQUIRED) catkin_metapackage() |
PS:CMakeLists.txt 中不可以有换行。
另请参考:
4.2 ROS节点运行管理launch文件
关于 launch 文件的使用我们已经不陌生了,在第一章内容中,就曾经介绍到:
一个程序中可能需要启动多个节点,比如:ROS 内置的小乌龟案例,如果要控制乌龟运动,要启动多个窗口,分别启动 roscore、乌龟界面节点、键盘控制节点。如果每次都调用 rosrun 逐一启动,显然效率低下,如何优化?
采用的优化策略便是使用roslaunch 命令集合 launch 文件启动管理节点,并且在后续教程中,也多次使用到了 launch 文件。
概念
launch 文件是一个 XML 格式的文件,可以启动本地和远程的多个节点,还可以在参数服务器中设置参数。
作用
简化节点的配置与启动,提高ROS程序的启动效率。
使用
以 turtlesim 为例演示
1.新建launch文件
在功能包下添加 launch目录, 目录下新建 xxxx.launch 文件,编辑 launch 文件
Plaintext <launch> <node pkg="turtlesim" type="turtlesim_node" name="myTurtle" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="myTurtleContro" output="screen" /> </launch> |
2.调用 launch 文件
Plaintext roslaunch 包名 xxx.launch |
注意:roslaunch 命令执行launch文件时,首先会判断是否启动了 roscore,如果启动了,则不再启动,否则,会自动调用 roscore
PS:本节主要介绍launch文件的使用语法,launch 文件中的标签,以及不同标签的一些常用属性。
另请参考:
4.2.1 launch文件标签之launch
<launch>标签是所有 launch 文件的根标签,充当其他标签的容器
1.属性
2.子级标签
所有其它标签都是launch的子级
4.2.2 launch文件标签之node
<node>标签用于指定 ROS 节点,是最常见的标签,需要注意的是: roslaunch 命令不能保证按照 node 的声明顺序来启动节点(节点的启动是多进程的)
1.属性
- respawn="true | false" (可选)
- 如果 respawn 为 true, 那么延迟 N 秒后启动节点
- required="true | false" (可选)
- 该节点是否必须,如果为 true,那么如果该节点退出,将杀死整个 roslaunch
- clear_params="true | false" (可选)
- output="log | screen" (可选)
- 日志发送目标,可以设置为 log 日志文件,或 screen 屏幕,默认是 log
2.子级标签
4.2.3 launch文件标签之include
include标签用于将另一个 xml 格式的 launch 文件导入到当前文件
1.属性
- file="$(find 包名)/xxx/xxx.launch"
2.子级标签
4.2.4 launch文件标签之remap
用于话题重命名
1.属性
2.子级标签
4.2.5 launch文件标签之param
<param>标签主要用于在参数服务器上设置参数,参数源可以在标签中通过 value 指定,也可以通过外部文件加载,在<node>标签中时,相当于私有命名空间。
1.属性
- 定义参数值,如果此处省略,必须指定外部文件作为参数源
- type="str | int | double | bool | yaml" (可选)
- 指定参数类型,如果未指定,roslaunch 会尝试确定参数类型,规则如下:
- "true" 和 "false" 是 bool 值(不区分大小写)
2.子级标签
4.2.6 launch文件标签之rosparam
<rosparam>标签可以从 YAML 文件导入参数,或将参数导出到 YAML 文件,也可以用来删除参数,<rosparam>标签在<node>标签中时被视为私有。
1.属性
- command="load | dump | delete" (可选,默认 load)
- file="$(find xxxxx)/xxx/yyy...."
2.子级标签
4.2.7 launch文件标签之group
<group>标签可以对节点分组,具有 ns 属性,可以让节点归属某个命名空间
1.属性
- clear_params="true | false" (可选)
- 启动前,是否删除组名称空间的所有参数(慎用....此功能危险)
2.子级标签
4.2.8 launch文件标签之arg
<arg>标签是用于动态传参,类似于函数的参数,可以增强launch文件的灵活性
1.属性
2.子级标签
3.示例
- launch文件传参语法实现,hello.lcaunch
Plain Text <launch> <arg name="xxx" /> <param name="param" value="$(arg xxx)" /> </launch> |
Plaintext roslaunch hello.launch xxx:=值 |
4.3 ROS工作空间覆盖
所谓工作空间覆盖,是指不同工作空间中,存在重名的功能包的情形。 ROS 开发中,会自定义工作空间且自定义工作空间可以同时存在多个,可能会出现一种情况: 虽然特定工作空间内的功能包不能重名,但是自定义工作空间的功能包与内置的功能包可以重名或者不同的自定义的工作空间中也可以出现重名的功能包,那么调用该名称功能包时,会调用哪一个呢?比如:自定义工作空间A存在功能包 turtlesim,自定义工作空间B也存在功能包 turtlesim,当然系统内置空间也存在turtlesim,如果调用turtlesim包,会调用哪个工作空间中的呢? |
实现
0.新建工作空间A与工作空间B,两个工作空间中都创建功能包: turtlesim。
1.在 ~/.bashrc 文件下追加当前工作空间的 bash 格式如下:
Plaintext source /home/用户/路径/工作空间A/devel/setup.bash source /home/用户/路径/工作空间B/devel/setup.bash |
2.新开命令行:source .bashrc加载环境变量
3.查看ROS环境环境变量echo $ROS_PACKAGE_PATH
结果:自定义工作空间B:自定义空间A:系统内置空间
4.调用命令:roscd turtlesim会进入自定义工作空间B
原因
ROS 会解析 .bashrc 文件,并生成 ROS_PACKAGE_PATH ROS包路径,该变量中按照 .bashrc 中配置设置工作空间优先级,在设置时需要遵循一定的原则:ROS_PACKAGE_PATH 中的值,和 .bashrc 的配置顺序相反--->后配置的优先级更高,如果更改自定义空间A与自定义空间B的source顺序,那么调用时,将进入工作空间A。
结论
功能包重名时,会按照 ROS_PACKAGE_PATH 查找,配置在前的会优先执行。
隐患
存在安全隐患,比如当前工作空间B优先级更高,意味着当程序调用 turtlesim 时,不会调用工作空间A也不会调用系统内置的 turtlesim,如果工作空间A在实现时有其他功能包依赖于自身的 turtlesim,而按照ROS工作空间覆盖的涉及原则,那么实际执行时将会调用工作空间B的turtlesim,从而导致执行异常,出现安全隐患。
BUG 说明: 当在 .bashrc 文件中 source 多个工作空间后,可能出现的情况,在 ROS PACKAGE PATH 中只包含两个工作空间,可以删除自定义工作空间的 build 与 devel 目录,重新 catkin_make,然后重新载入 .bashrc 文件,问题解决。 |
4.4 ROS节点名称重名
场景:ROS 中创建的节点是有名称的,C++初始化节点时通过API:ros::init(argc,argv,"xxxx");来定义节点名称,在Python中初始化节点则通过 rospy.init_node("yyyy") 来定义节点名称。在ROS的网络拓扑中,是不可以出现重名的节点的,因为假设可以重名存在,那么调用时会产生混淆,这也就意味着,不可以启动重名节点或者同一个节点启动多次,的确,在ROS中如果启动重名节点的话,之前已经存在的节点会被直接关闭,但是如果有这种需求的话,怎么优化呢? |
在ROS中给出的解决策略是使用命名空间或名称重映射。
命名空间就是为名称添加前缀,名称重映射是为名称起别名。这两种策略都可以解决节点重名问题,两种策略的实现途径有多种:
以上三种途径都可以通过命名空间或名称重映射的方式,来避免节点重名,本节将对三者的使用逐一演示,三者要实现的需求类似。
案例
启动两个 turtlesim_node 节点,当然如果直接打开两个终端,直接启动,那么第一次启动的节点会关闭,并给出提示:
Plaintext [ WARN] [1578812836.351049332]: Shutdown request received. [ WARN] [1578812836.351207362]: Reason given for shutdown: [new node registered with same name] |
因为两个节点不能重名,接下来将会介绍解决重名问题的多种方案。
4.4.1 rosrun设置命名空间与重映射
1.rosrun设置命名空间
1.1设置命名空间演示
语法: rosrun 包名 节点名 __ns:=新名称
Plaintext rosrun turtlesim turtlesim_node __ns:=/xxx |
Plaintext rosrun turtlesim turtlesim_node __ns:=/yyy |
两个节点都可以正常运行
1.2运行结果
rosnode list查看节点信息,显示结果:
Plaintext /xxx/turtlesim /yyy/turtlesim |
2.rosrun名称重映射
2.1为节点起别名
语法: rosrun 包名 节点名 __name:=新名称
Plaintext rosrun turtlesim turtlesim_node __name:=t1 | rosrun turtlesim turtlesim_node /turtlesim:=t1(不适用于python) |
Plaintext rosrun turtlesim turtlesim_node __name:=t2 | rosrun turtlesim turtlesim_node /turtlesim:=t2(不适用于python) |
两个节点都可以运行
2.2运行结果
rosnode list查看节点信息,显示结果:
3.rosrun命名空间与名称重映射叠加
3.1设置命名空间同时名称重映射
语法: rosrun 包名 节点名 __ns:=新名称 __name:=新名称
Plaintext rosrun turtlesim turtlesim_node __ns:=/xxx __name:=tn |
3.2运行结果
rosnode list查看节点信息,显示结果:
使用环境变量也可以设置命名空间,启动节点前在终端键入如下命令: export ROS_NAMESPACE=xxxx |
4.4.2 launch文件设置命名空间与重映射
介绍 launch 文件的使用语法时,在 node 标签中有两个属性: name 和 ns,二者分别是用于实现名称重映射与命名空间设置的。使用launch文件设置命名空间与名称重映射也比较简单。
1.launch文件
Plaintext <launch>
<node pkg="turtlesim" type="turtlesim_node" name="t1" /> <node pkg="turtlesim" type="turtlesim_node" name="t2" /> <node pkg="turtlesim" type="turtlesim_node" name="t1" ns="hello"/>
</launch> |
在 node 标签中,name 属性是必须的,ns 可选。
2.运行
rosnode list查看节点信息,显示结果:
Plaintext /t1 /t2 /t1/hello |
4.4.3 编码设置命名空间与重映射
如果自定义节点实现,那么可以更灵活的设置命名空间与重映射实现。
1.C++ 实现:重映射
1.1名称别名设置
核心代码:ros::init(argc,argv,"zhangsan",ros::init_options::AnonymousName);
1.2执行
会在名称后面添加时间戳。
2.C++ 实现:命名空间
2.1命名空间设置
核心代码
Plaintext std::map<std::string, std::string> map; map["__ns"] = "xxxx"; ros::init(map,"wangqiang"); |
2.2执行
节点名称设置了命名空间。
3.Python 实现:重映射
3.1名称别名设置
核心代码:rospy.init_node("lisi",anonymous=True)
3.2执行
会在节点名称后缀时间戳。
4.5 ROS话题名称设置
在ROS中节点名称可能出现重名的情况,同理话题名称也可能重名。
在 ROS 中节点终端,不同的节点之间通信都依赖于话题,话题名称也可能出现重复的情况,这种情况下,系统虽然不会抛出异常,但是可能导致订阅的消息非预期的,从而导致节点运行异常。这种情况下需要将两个节点的话题名称由相同修改为不同。
又或者,两个节点是可以通信的,两个节点之间使用了相同的消息类型,但是由于,话题名称不同,导致通信失败。这种情况下需要将两个节点的话题名称由不同修改为相同。
在实际应用中,按照逻辑,有些时候可能需要将相同的话题名称设置为不同,也有可能将不同的话题名设置为相同。在ROS中给出的解决策略与节点名称重命类似,也是使用名称重映射或为名称添加前缀。根据前缀不同,有全局、相对、和私有三种类型之分。
- 全局(参数名称直接参考ROS系统,与节点命名空间平级)
- 相对(参数名称参考的是节点的命名空间,与节点名称平级)
名称重映射是为名称起别名,为名称添加前缀,该实现比节点重名更复杂些,不单是使用命名空间作为前缀、还可以使用节点名称最为前缀。两种策略的实现途径有多种:
本节将对三者的使用逐一演示,三者要实现的需求类似。
4.5.1 rosrun设置话题重映射
rosrun名称重映射语法: rorun 包名 节点名 话题名:=新话题名称
实现teleop_twist_keyboard与乌龟显示节点通信方案由两种:
1.方案1
将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel
启动键盘控制节点:rosrun teleop_twist_keyboard teleop_twist_keyboard.py /cmd_vel:=/turtle1/cmd_vel
启动乌龟显示节点: rosrun turtlesim turtlesim_node
二者可以实现正常通信
2.方案2
将乌龟显示节点的话题设置为 /cmd_vel
启动键盘控制节点:rosrun teleop_twist_keyboard teleop_twist_keyboard.py
启动乌龟显示节点: rosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel
二者可以实现正常通信
4.5.2 launch文件设置话题重映射
launch 文件设置话题重映射语法:
HTML <node pkg="xxx" type="xxx" name="xxx"> <remap from="原话题" to="新话题" /> </node> |
实现teleop_twist_keyboard与乌龟显示节点通信方案由两种:
1.方案1
将 teleop_twist_keyboard 节点的话题设置为/turtle1/cmd_vel
HTML <launch>
<node pkg="turtlesim" type="turtlesim_node" name="t1" /> <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key"> <remap from="/cmd_vel" to="/turtle1/cmd_vel" /> </node>
</launch> |
二者可以实现正常通信
2.方案2
将乌龟显示节点的话题设置为 /cmd_vel
HTML <launch> <node pkg="turtlesim" type="turtlesim_node" name="t1"> <remap from="/turtle1/cmd_vel" to="/cmd_vel" /> </node> <node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key" />
</launch> |
二者可以实现正常通信
4.5.3 编码设置话题名称
话题的名称与节点的命名空间、节点的名称是有一定关系的,话题名称大致可以分为三种类型:
- 相对(话题参考的是节点的命名空间,与节点名称平级)
结合编码演示具体关系。
C++ 实现
演示准备:
1.初始化节点设置一个节点名称
ros::init(argc,argv,"hello")
2.设置不同类型的话题
3.启动节点时,传递一个 __ns:= xxx
4.节点启动后,使用 rostopic 查看话题信息
1.1全局名称
格式:以/开头的名称,和节点名称无关
比如:/xxx/yyy/zzz
示例1:ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter",1000);
结果1:/chatter
示例2:ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money",1000);
结果2:/chatter/money
1.2相对名称
格式:非/开头的名称,参考命名空间(与节点名称平级)来确定话题名称
示例1:ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);
结果1:xxx/chatter
示例2:ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money",1000);
结果2:xxx/chatter/money
1.3私有名称
格式:以~开头的名称
示例1:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",1000);
结果1:/xxx/hello/chatter
示例2:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money",1000);
结果2:/xxx/hello/chatter/money
PS:当使用~,而话题名称有时/开头时,那么话题名称是绝对的
示例3:
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money",1000);
结果3:/chatter/money
4.6 ROS参数名称设置
在ROS中节点名称话题名称可能出现重名的情况,同理参数名称也可能重名。
当参数名称重名时,那么就会产生覆盖,如何避免这种情况?
关于参数重名的处理,没有重映射实现,为了尽量的避免参数重名,都是使用为参数名添加前缀的方式,实现类似于话题名称,有全局、相对、和私有三种类型之分。
- 全局(参数名称直接参考ROS系统,与节点命名空间平级)
- 相对(参数名称参考的是节点的命名空间,与节点名称平级)
设置参数的方式也有三种:
三种设置方式前面都已经有所涉及,但是之前没有涉及命名问题,本节将对三者命名的设置逐一演示。
案例
启动节点时,为参数服务器添加参数(需要注意参数名称设置)。
4.6.1 rosrun设置参数
rosrun 在启动节点时,也可以设置参数:
语法: rosrun 包名 节点名称 _参数名:=参数值
1.设置参数
启动乌龟显示节点,并设置参数 A = 100
Plaintext rosrun turtlesim turtlesim_node _A:=100 |
2.运行
rosparam list查看节点信息,显示结果:
Plaintext /turtlesim/A /turtlesim/background_b /turtlesim/background_g /turtlesim/background_r |
结果显示,参数A前缀节点名称,也就是说rosrun执行设置参数参数名使用的是私有模式
4.6.2 launch文件设置参数
通过 launch 文件设置参数的方式前面已经介绍过了,可以在 node 标签外,或 node 标签中通过 param 或 rosparam 来设置参数。在 node 标签外设置的参数是全局性质的,参考的是 / ,在 node 标签中设置的参数是私有性质的,参考的是 /命名空间/节点名称。
1.设置参数
以 param 标签为例,设置参数
HTML <launch>
<param name="p1" value="100" /> <node pkg="turtlesim" type="turtlesim_node" name="t1"> <param name="p2" value="100" /> </node>
</launch> |
2.运行
rosparam list查看节点信息,显示结果:
运行结果与预期一致。
4.6.3 编码设置参数
编码的方式可以更方便的设置:全局、相对与私有参数。
C++实现
在 C++ 中,可以使用 ros::param 或者 ros::NodeHandle 来设置参数。
1.1 ros::param设置参数
设置参数调用API是ros::param::set,该函数中,参数1传入参数名称,参数2是传入参数值,参数1中参数名称设置时,如果以 / 开头,那么就是全局参数,如果以 ~ 开头,那么就是私有参数,既不以 / 也不以 ~ 开头,那么就是相对参数。代码示例:
C++ ros::param::set("/set_A",100); //全局,和命名空间以及节点名称无关 ros::param::set("set_B",100); //相对,参考命名空间 ros::param::set("~set_C",100); //私有,参考命名空间与节点名称 |
运行时,假设设置的 namespace 为 xxx,节点名称为 yyy,使用 rosparam list 查看:
C++ /set_A /xxx/set_B /xxx/yyy/set_C |
1.2 ros::NodeHandle设置参数
设置参数时,首先需要创建 NodeHandle 对象,然后调用该对象的 setParam 函数,该函数参数1为参数名,参数2为要设置的参数值,如果参数名以 / 开头,那么就是全局参数,如果参数名不以 / 开头,那么,该参数是相对参数还是私有参数与NodeHandle 对象有关,如果NodeHandle 对象创建时如果是调用的默认的无参构造,那么该参数是相对参数,如果NodeHandle 对象创建时是使用:
ros::NodeHandle nh("~"),那么该参数就是私有参数。代码示例:
C++ ros::NodeHandle nh; nh.setParam("/nh_A",100); //全局,和命名空间以及节点名称无关 nh.setParam("nh_B",100); //相对,参考命名空间 ros::NodeHandle nh_private("~"); nh_private.setParam("nh_C",100);//私有,参考命名空间与节点名称 |
运行时,假设设置的 namespace 为 xxx,节点名称为 yyy,使用 rosparam list 查看:
Plaintext /nh_A /xxx/nh_B /xxx/yyy/nh_C |
4.7 ROS分布式通信
ROS是一个分布式计算环境。一个运行中的ROS系统可以包含分布在多台计算机上多个节点。根据系统的配置方式,任何节点可能随时需要与任何其他节点进行通信。
因此,ROS对网络配置有某些要求:
- 每台计算机必须通过所有其他计算机都可以解析的名称来公告自己。
实现
1.准备
先要保证不同计算机处于同一网络中,最好分别设置固定IP,如果为虚拟机,需要将网络适配器改为桥接模式;
2.配置文件修改
分别修改不同计算机的 /etc/hosts 文件,在该文件中加入对方的IP地址和计算机名:
主机端:
从机端:
设置完毕,可以通过 ping 命令测试网络通信是否正常。 IP地址查看名: ifconfig 计算机名称查看: hostname |
3.配置主机IP
配置主机的 IP 地址
~/.bashrc 追加
Plaintext export ROS_MASTER_URI=http://主机IP:11311 export ROS_HOSTNAME=主机IP |
4.配置从机IP
配置从机的 IP 地址,从机可以有多台,每台都做如下设置:
~/.bashrc 追加
Plaintext export ROS_MASTER_URI=http://主机IP:11311 export ROS_HOSTNAME=从机IP |
测试
1.主机启动 roscore(必须)
2.主机启动订阅节点,从机启动发布节点,测试通信是否正常
3.反向测试,主机启动发布节点,从机启动订阅节点,测试通信是否正常
第 5 章 ROS常用组件
在ROS中内置一些比较实用的工具,通过这些工具可以方便快捷的实现某个功能或调试程序,从而提高开发效率,本章主要介绍ROS中内置的如下组件:
- rosbag 用于录制ROS节点的执行过程并可以重放该过程;
本章预期达成的学习目标:
- 可以使用 rosbag 命令或编码的形式实现录制与回放;
案例演示: 小乌龟跟随实现,该案例是ros中内置案例,终端下键入启动命令
roslaunch turtle_tf2 turtle_tf2_demo_cpp.launch或roslaunch turtle_tf2 turtle_tf2_demo.launch
键盘可以控制一只乌龟运动,另一只跟随运动。

5.1 TF坐标变换
机器人系统上,有多个传感器,如激光雷达、摄像头等,有的传感器是可以感知机器人周边的物体方位(或者称之为:坐标,横向、纵向、高度的距离信息)的,以协助机器人定位障碍物,可以直接将物体相对该传感器的方位信息,等价于物体相对于机器人系统或机器人其它组件的方位信息吗?显示是不行的,这中间需要一个转换过程。更具体描述如下: 场景1:雷达与小车 现有一移动式机器人底盘,在底盘上安装了一雷达,雷达相对于底盘的偏移量已知,现雷达检测到一障碍物信息,获取到坐标分别为(x,y,z),该坐标是以雷达为参考系的,如何将这个坐标转换成以小车为参考系的坐标呢? |


场景2:现有一带机械臂的机器人(比如:PR2)需要夹取目标物,当前机器人头部摄像头可以探测到目标物的坐标(x,y,z),不过该坐标是以摄像头为参考系的,而实际操作目标物的是机械臂的夹具,当前我们需要将该坐标转换成相对于机械臂夹具的坐标,这个过程如何实现? |

当然,根据我们高中学习的知识,在明确了不同坐标系之间的的相对关系,就可以实现任何坐标点在不同坐标系之间的转换,但是该计算实现是较为常用的,且算法也有点复杂,因此在 ROS 中直接封装了相关的模块: 坐标变换(TF)。
概念
tf:TransForm Frame,坐标变换
坐标系:ROS 中是通过坐标系统开标定物体的,确切的将是通过右手坐标系来标定的。

作用
在 ROS 中用于实现不同坐标系之间的点或向量的转换。
案例
小乌龟跟随案例:如本章引言部分演示。
说明
在ROS中坐标变换最初对应的是tf,不过在 hydro 版本开始, tf 被弃用,迁移到 tf2,后者更为简洁高效,tf2对应的常用功能包有:
tf2_geometry_msgs:可以将ROS消息转换成tf2消息。
tf2: 封装了坐标变换的常用消息。
tf2_ros:为tf2提供了roscpp和rospy绑定,封装了坐标变换常用的API。
另请参考:
5.1.1 坐标msg消息
订阅发布模型中数据载体 msg 是一个重要实现,首先需要了解一下,在坐标转换实现中常用的 msg:geometry_msgs/TransformStamped和geometry_msgs/PointStamped
前者用于传输坐标系相关位置信息,后者用于传输某个坐标系内坐标点的信息。在坐标变换中,频繁的需要使用到坐标系的相对关系以及坐标点信息。
1.geometry_msgs/TransformStamped
命令行键入:rosmsg info geometry_msgs/TransformStamped
Plaintext std_msgs/Header header #头信息 uint32 seq #|-- 序列号 time stamp #|-- 时间戳 string frame_id #|-- 坐标 ID string child_frame_id #子坐标系的 id geometry_msgs/Transform transform #坐标信息 geometry_msgs/Vector3 translation #偏移量 float64 x #|-- X 方向的偏移量 float64 y #|-- Y 方向的偏移量 float64 z #|-- Z 方向上的偏移量 geometry_msgs/Quaternion rotation #四元数 float64 x float64 y float64 z float64 w |
四元数用于表示坐标的相对姿态
2.geometry_msgs/PointStamped
命令行键入:rosmsg info geometry_msgs/PointStamped
Plaintext std_msgs/Header header #头 uint32 seq #|-- 序号 time stamp #|-- 时间戳 string frame_id #|-- 所属坐标系的 id geometry_msgs/Point point #点坐标 float64 x #|-- x y z 坐标 float64 y float64 z |
另请参考:
5.1.2 静态坐标变换
所谓静态坐标变换,是指两个坐标系之间的相对位置是固定的。
需求描述:
现有一机器人模型,核心构成包含主体与雷达,各对应一坐标系,坐标系的原点分别位于主体与雷达的物理中心,已知雷达原点相对于主体原点位移关系如下: x 0.2 y0.0 z0.5。当前雷达检测到一障碍物,在雷达坐标系中障碍物的坐标为 (2.0 3.0 5.0),请问,该障碍物相对于主体的坐标是多少?
结果演示:


实现分析:
- 坐标系相对关系,可以通过发布方发布
- 订阅方,订阅到发布的坐标系相对关系,再传入坐标点信息(可以写死),然后借助于 tf 实现坐标变换,并将结果输出
实现流程:C++ 与 Python 实现流程一致
- 新建功能包,添加依赖
- 编写发布方实现
- 编写订阅方实现
- 执行并查看结果
C++实现
1.创建功能包
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs
2.发布方
C++ /* 静态坐标变换发布方: 发布关于 laser 坐标系的位置信息
实现流程: 1.包含头文件 2.初始化 ROS 节点 3.创建静态坐标转换广播器 4.创建坐标系信息 5.广播器发布坐标系信息 6.spin() */
// 1.包含头文件 #include "ros/ros.h" #include "tf2_ros/static_transform_broadcaster.h" #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"static_brocast"); // 3.创建静态坐标转换广播器 tf2_ros::StaticTransformBroadcaster broadcaster; // 4.创建坐标系信息 geometry_msgs::TransformStamped ts; //----设置头信息 ts.header.seq = 100; ts.header.stamp = ros::Time::now(); ts.header.frame_id = "base_link"; //----设置子级坐标系 ts.child_frame_id = "laser"; //----设置子级相对于父级的偏移量 ts.transform.translation.x = 0.2; ts.transform.translation.y = 0.0; ts.transform.translation.z = 0.5; //----设置四元数:将 欧拉角数据转换成四元数 tf2::Quaternion qtn; qtn.setRPY(0,0,0); ts.transform.rotation.x = qtn.getX(); ts.transform.rotation.y = qtn.getY(); ts.transform.rotation.z = qtn.getZ(); ts.transform.rotation.w = qtn.getW(); // 5.广播器发布坐标系信息 broadcaster.sendTransform(ts); ros::spin(); return 0; } |
配置文件此处略。
3.订阅方
C++ /* 订阅坐标系信息,生成一个相对于 子级坐标系的坐标点数据,转换成父级坐标系中的坐标点
实现流程: 1.包含头文件 2.初始化 ROS 节点 3.创建 TF 订阅节点 4.生成一个坐标点(相对于子级坐标系) 5.转换坐标点(相对于父级坐标系) 6.spin() */ //1.包含头文件 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/PointStamped.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" //注意: 调用 transform 必须包含该头文件
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"tf_sub"); ros::NodeHandle nh; // 3.创建 TF 订阅节点 tf2_ros::Buffer buffer; tf2_ros::TransformListener listener(buffer);
ros::Rate r(1); while (ros::ok()) { // 4.生成一个坐标点(相对于子级坐标系) geometry_msgs::PointStamped point_laser; point_laser.header.frame_id = "laser"; point_laser.header.stamp = ros::Time::now(); point_laser.point.x = 1; point_laser.point.y = 2; point_laser.point.z = 7.3; // 5.转换坐标点(相对于父级坐标系) //新建一个坐标点,用于接收转换结果 //--------------使用 try 语句或休眠,否则可能由于缓存接收延迟而导致坐标转换失败------------------------ try { geometry_msgs::PointStamped point_base; point_base = buffer.transform(point_laser,"base_link"); ROS_INFO("转换后的数据:(%.2f,%.2f,%.2f),参考的坐标系是:",point_base.point.x,point_base.point.y,point_base.point.z,point_base.header.frame_id.c_str());
} catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("程序异常....."); }
r.sleep(); ros::spinOnce(); }
return 0; } |
配置文件此处略。
4.执行
可以使用命令行或launch文件的方式分别启动发布节点与订阅节点,如果程序无异常,控制台将输出,坐标转换后的结果。
5.1.3 动态坐标变换
所谓动态坐标变换,是指两个坐标系之间的相对位置是变化的。
需求描述:
启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘控制乌龟运动,将两个坐标系的相对位置动态发布。
结果演示:

实现分析:
- 乌龟本身不但可以看作坐标系,也是世界坐标系中的一个坐标点
- 订阅 turtle1/pose,可以获取乌龟在世界坐标系的 x坐标、y坐标、偏移量以及线速度和角速度
- 将 pose 信息转换成 坐标系相对信息并发布
实现流程:C++ 与 Python 实现流程一致
- 新建功能包,添加依赖
- 创建坐标相对关系发布方(同时需要订阅乌龟位姿信息)
- 创建坐标相对关系订阅方
- 执行
C++实现
1.创建功能包
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
2.发布方
C++ /* 动态的坐标系相对姿态发布(一个坐标系相对于另一个坐标系的相对姿态是不断变动的)
需求: 启动 turtlesim_node,该节点中窗体有一个世界坐标系(左下角为坐标系原点),乌龟是另一个坐标系,键盘 控制乌龟运动,将两个坐标系的相对位置动态发布
实现分析: 1.乌龟本身不但可以看作坐标系,也是世界坐标系中的一个坐标点 2.订阅 turtle1/pose,可以获取乌龟在世界坐标系的 x坐标、y坐标、偏移量以及线速度和角速度 3.将 pose 信息转换成 坐标系相对信息并发布
实现流程: 1.包含头文件 2.初始化 ROS 节点 3.创建 ROS 句柄 4.创建订阅对象 5.回调函数处理订阅到的数据(实现TF广播) 5-1.创建 TF 广播器 5-2.创建 广播的数据(通过 pose 设置) 5-3.广播器发布数据 6.spin */ // 1.包含头文件 #include "ros/ros.h" #include "turtlesim/Pose.h" #include "tf2_ros/transform_broadcaster.h" #include "geometry_msgs/TransformStamped.h" #include "tf2/LinearMath/Quaternion.h"
void doPose(const turtlesim::Pose::ConstPtr& pose){ // 5-1.创建 TF 广播器 static tf2_ros::TransformBroadcaster broadcaster; // 5-2.创建 广播的数据(通过 pose 设置) geometry_msgs::TransformStamped tfs; // |----头设置 tfs.header.frame_id = "world"; tfs.header.stamp = ros::Time::now();
// |----坐标系 ID tfs.child_frame_id = "turtle1";
// |----坐标系相对信息设置 tfs.transform.translation.x = pose->x; tfs.transform.translation.y = pose->y; tfs.transform.translation.z = 0.0; // 二维实现,pose 中没有z,z 是 0 // |--------- 四元数设置 tf2::Quaternion qtn; qtn.setRPY(0,0,pose->theta); tfs.transform.rotation.x = qtn.getX(); tfs.transform.rotation.y = qtn.getY(); tfs.transform.rotation.z = qtn.getZ(); tfs.transform.rotation.w = qtn.getW();
// 5-3.广播器发布数据 broadcaster.sendTransform(tfs); }
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"dynamic_tf_pub"); // 3.创建 ROS 句柄 ros::NodeHandle nh; // 4.创建订阅对象 ros::Subscriber sub = nh.subscribe<turtlesim::Pose>("/turtle1/pose",1000,doPose); // 5.回调函数处理订阅到的数据(实现TF广播) // // 6.spin ros::spin(); return 0; } |
配置文件此处略。
3.订阅方
C++ //1.包含头文件 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2_ros/buffer.h" #include "geometry_msgs/PointStamped.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" //注意: 调用 transform 必须包含该头文件
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ROS 节点 ros::init(argc,argv,"dynamic_tf_sub"); ros::NodeHandle nh; // 3.创建 TF 订阅节点 tf2_ros::Buffer buffer; tf2_ros::TransformListener listener(buffer);
ros::Rate r(1); while (ros::ok()) { // 4.生成一个坐标点(相对于子级坐标系) geometry_msgs::PointStamped point_laser; point_laser.header.frame_id = "turtle1"; point_laser.header.stamp = ros::Time(); point_laser.point.x = 1; point_laser.point.y = 1; point_laser.point.z = 0; // 5.转换坐标点(相对于父级坐标系) //新建一个坐标点,用于接收转换结果 //--------------使用 try 语句或休眠,否则可能由于缓存接收延迟而导致坐标转换失败------------------------ try { geometry_msgs::PointStamped point_base; point_base = buffer.transform(point_laser,"world"); ROS_INFO("坐标点相对于 world 的坐标为:(%.2f,%.2f,%.2f)",point_base.point.x,point_base.point.y,point_base.point.z);
} catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("程序异常:%s",e.what()); }
r.sleep(); ros::spinOnce(); }
return 0; } |
配置文件此处略。
4.执行
可以使用命令行或launch文件的方式分别启动发布节点与订阅节点,如果程序无异常,与演示结果类似。
可以使用 rviz 查看坐标系相对关系。
5.1.4 多坐标变换
需求描述:
现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2,son1 相对于 world,以及 son2 相对于 world 的关系是已知的,求 son1原点在 son2中的坐标,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标
实现分析:
- 首先,需要发布 son1 相对于 world,以及 son2 相对于 world 的坐标消息
- 然后,需要订阅坐标发布消息,并取出订阅的消息,借助于 tf2 实现 son1 和 son2 的转换
- 最后,还要实现坐标点的转换
实现流程:C++ 与 Python 实现流程一致
- 新建功能包,添加依赖
- 创建坐标相对关系发布方(需要发布两个坐标相对关系)
- 创建坐标相对关系订阅方
- 执行
C++实现
1.创建功能包
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
2.发布方
为了方便,使用静态坐标变换发布
HTML <launch> <node pkg="tf2_ros" type="static_transform_publisher" name="son1" args="0.2 0.8 0.3 0 0 0 /world /son1" output="screen" /> <node pkg="tf2_ros" type="static_transform_publisher" name="son2" args="0.5 0 0 0 0 0 /world /son2" output="screen" /> </launch> |
3.订阅方
C++ /*
需求: 现有坐标系统,父级坐标系统 world,下有两子级系统 son1,son2, son1 相对于 world,以及 son2 相对于 world 的关系是已知的, 求 son1 与 son2中的坐标关系,又已知在 son1中一点的坐标,要求求出该点在 son2 中的坐标 实现流程: 1.包含头文件 2.初始化 ros 节点 3.创建 ros 句柄 4.创建 TF 订阅对象 5.解析订阅信息中获取 son1 坐标系原点在 son2 中的坐标 解析 son1 中的点相对于 son2 的坐标 6.spin
*/ //1.包含头文件 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "tf2/LinearMath/Quaternion.h" #include "tf2_geometry_msgs/tf2_geometry_msgs.h" #include "geometry_msgs/TransformStamped.h" #include "geometry_msgs/PointStamped.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ros 节点 ros::init(argc,argv,"sub_frames"); // 3.创建 ros 句柄 ros::NodeHandle nh; // 4.创建 TF 订阅对象 tf2_ros::Buffer buffer; tf2_ros::TransformListener listener(buffer); // 5.解析订阅信息中获取 son1 坐标系原点在 son2 中的坐标 ros::Rate r(1); while (ros::ok()) { try { // 解析 son1 中的点相对于 son2 的坐标 geometry_msgs::TransformStamped tfs = buffer.lookupTransform("son2","son1",ros::Time(0)); ROS_INFO("Son1 相对于 Son2 的坐标关系:父坐标系ID=%s",tfs.header.frame_id.c_str()); ROS_INFO("Son1 相对于 Son2 的坐标关系:子坐标系ID=%s",tfs.child_frame_id.c_str()); ROS_INFO("Son1 相对于 Son2 的坐标关系:x=%.2f,y=%.2f,z=%.2f", tfs.transform.translation.x, tfs.transform.translation.y, tfs.transform.translation.z );
// 坐标点解析 geometry_msgs::PointStamped ps; ps.header.frame_id = "son1"; ps.header.stamp = ros::Time::now(); ps.point.x = 1.0; ps.point.y = 2.0; ps.point.z = 3.0;
geometry_msgs::PointStamped psAtSon2; psAtSon2 = buffer.transform(ps,"son2"); ROS_INFO("在 Son2 中的坐标:x=%.2f,y=%.2f,z=%.2f", psAtSon2.point.x, psAtSon2.point.y, psAtSon2.point.z ); } catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("异常信息:%s",e.what()); }
r.sleep(); // 6.spin ros::spinOnce(); } return 0; } |
配置文件此处略。
4.执行
可以使用命令行或launch文件的方式分别启动发布节点与订阅节点,如果程序无异常,将输出换算后的结果。
5.1.5 坐标系关系查看
在机器人系统中,涉及的坐标系有多个,为了方便查看,ros 提供了专门的工具,可以用于生成显示坐标系关系的 pdf 文件,该文件包含树形结构的坐标系图谱。
6.1准备
首先调用rospack find tf2_tools查看是否包含该功能包,如果没有,请使用如下命令安装:
Plaintext sudo apt install ros-noetic-tf2-tools |
6.2使用
6.2.1生成 pdf 文件
启动坐标系广播程序之后,运行如下命令:
Plaintext rosrun tf2_tools view_frames.py |
会产生类似于下面的日志信息:
Plaintext [INFO] [1592920556.827549]: Listening to tf data during 5 seconds... [INFO] [1592920561.841536]: Generating graph in frames.pdf file... |
查看当前目录会生成一个 frames.pdf 文件
6.2.2查看文件
可以直接进入目录打开文件,或者调用命令查看文件:evince frames.pdf
内如如图所示:

另请参考:
5.1.6 TF坐标变换实操
需求描述:
程序启动之初: 产生两只乌龟,中间的乌龟(A) 和 左下乌龟(B), B 会自动运行至A的位置,并且键盘控制时,只是控制 A 的运动,但是 B 可以跟随 A 运行
结果演示:

实现分析:
乌龟跟随实现的核心,是乌龟A和B都要发布相对世界坐标系的坐标信息,然后,订阅到该信息需要转换获取A相对于B坐标系的信息,最后,再生成速度信息,并控制B运动。
- 启动乌龟显示节点
- 在乌龟显示窗体中生成一只新的乌龟(需要使用服务)
- 编写两只乌龟发布坐标信息的节点
- 编写订阅节点订阅坐标信息并生成新的相对关系生成速度信息
实现流程:C++ 与 Python 实现流程一致
- 新建功能包,添加依赖
- 编写服务客户端,用于生成一只新的乌龟
- 编写发布方,发布两只乌龟的坐标信息
- 编写订阅方,订阅两只乌龟信息,生成速度信息并发布
- 运行
准备工作:
1.了解如何创建第二只乌龟,且不受键盘控制
创建第二只乌龟需要使用rosservice,话题使用的是 spawn
Plaintext rosservice call /spawn "x: 1.0 y: 1.0 theta: 1.0 name: 'turtle_flow'" name: "turtle_flow" |
键盘是无法控制第二只乌龟运动的,因为使用的话题: /第二只乌龟名称/cmd_vel,对应的要控制乌龟运动必须发布对应的话题消息
2.了解如何获取两只乌龟的坐标
是通过话题 /乌龟名称/pose 来获取的
Plaintext x: 1.0 //x坐标 y: 1.0 //y坐标 theta: -1.21437060833 //角度 linear_velocity: 0.0 //线速度 angular_velocity: 1.0 //角速度 |
C++实现
1.创建功能包
创建项目功能包依赖于 tf2、tf2_ros、tf2_geometry_msgs、roscpp rospy std_msgs geometry_msgs、turtlesim
2.服务客户端(生成乌龟)
C++ /* 创建第二只小乌龟 */ #include "ros/ros.h" #include "turtlesim/Spawn.h"
int main(int argc, char *argv[]) {
setlocale(LC_ALL,"");
//执行初始化 ros::init(argc,argv,"create_turtle"); //创建节点 ros::NodeHandle nh; //创建服务客户端 ros::ServiceClient client = nh.serviceClient<turtlesim::Spawn>("/spawn");
ros::service::waitForService("/spawn"); turtlesim::Spawn spawn; spawn.request.name = "turtle2"; spawn.request.x = 1.0; spawn.request.y = 2.0; spawn.request.theta = 3.12415926; bool flag = client.call(spawn); if (flag) { ROS_INFO("乌龟%s创建成功!",spawn.response.name.c_str()); } else { ROS_INFO("乌龟2创建失败!"); }
ros::spin();
return 0; } |
配置文件此处略。
3.发布方(发布两只乌龟的坐标信息)
可以订阅乌龟的位姿信息,然后再转换成坐标信息,两只乌龟的实现逻辑相同,只是订阅的话题名称,生成的坐标信息等稍有差异,可以将差异部分通过参数传入:
- 每次启动时都需要传入乌龟节点名称(第一次是 turtle1 第二次是 turtle2)
C++ /* 该文件实现:需要订阅 turtle1 和 turtle2 的 pose,然后广播相对 world 的坐标系信息
注意: 订阅的两只 turtle,除了命名空间(turtle1 和 turtle2)不同外, 其他的话题名称和实现逻辑都是一样的, 所以我们可以将所需的命名空间通过 args 动态传入
实现流程: 1.包含头文件 2.初始化 ros 节点 3.解析传入的命名空间 4.创建 ros 句柄 5.创建订阅对象 6.回调函数处理订阅的 pose 信息 6-1.创建 TF 广播器 6-2.将 pose 信息转换成 TransFormStamped 6-3.发布 7.spin
*/ //1.包含头文件 #include "ros/ros.h" #include "turtlesim/Pose.h" #include "tf2_ros/transform_broadcaster.h" #include "tf2/LinearMath/Quaternion.h" #include "geometry_msgs/TransformStamped.h" //保存乌龟名称 std::string turtle_name;
void doPose(const turtlesim::Pose::ConstPtr& pose){ // 6-1.创建 TF 广播器 ---------------------------------------- 注意 static static tf2_ros::TransformBroadcaster broadcaster; // 6-2.将 pose 信息转换成 TransFormStamped geometry_msgs::TransformStamped tfs; tfs.header.frame_id = "world"; tfs.header.stamp = ros::Time::now(); tfs.child_frame_id = turtle_name; tfs.transform.translation.x = pose->x; tfs.transform.translation.y = pose->y; tfs.transform.translation.z = 0.0; tf2::Quaternion qtn; qtn.setRPY(0,0,pose->theta); tfs.transform.rotation.x = qtn.getX(); tfs.transform.rotation.y = qtn.getY(); tfs.transform.rotation.z = qtn.getZ(); tfs.transform.rotation.w = qtn.getW(); // 6-3.发布 broadcaster.sendTransform(tfs);
}
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ros 节点 ros::init(argc,argv,"pub_tf"); // 3.解析传入的命名空间 if (argc != 2) { ROS_ERROR("请传入正确的参数"); } else { turtle_name = argv[1]; ROS_INFO("乌龟 %s 坐标发送启动",turtle_name.c_str()); }
// 4.创建 ros 句柄 ros::NodeHandle nh; // 5.创建订阅对象 ros::Subscriber sub = nh.subscribe<turtlesim::Pose>(turtle_name + "/pose",1000,doPose); // 6.回调函数处理订阅的 pose 信息 // 6-1.创建 TF 广播器 // 6-2.将 pose 信息转换成 TransFormStamped // 6-3.发布 // 7.spin ros::spin(); return 0; } |
配置文件此处略。
4.订阅方(解析坐标信息并生成速度信息)
C++ /* 订阅 turtle1 和 turtle2 的 TF 广播信息,查找并转换时间最近的 TF 信息 将 turtle1 转换成相对 turtle2 的坐标,在计算线速度和角速度并发布
实现流程: 1.包含头文件 2.初始化 ros 节点 3.创建 ros 句柄 4.创建 TF 订阅对象 5.处理订阅到的 TF 6.spin
*/ //1.包含头文件 #include "ros/ros.h" #include "tf2_ros/transform_listener.h" #include "geometry_msgs/TransformStamped.h" #include "geometry_msgs/Twist.h"
int main(int argc, char *argv[]) { setlocale(LC_ALL,""); // 2.初始化 ros 节点 ros::init(argc,argv,"sub_TF"); // 3.创建 ros 句柄 ros::NodeHandle nh; // 4.创建 TF 订阅对象 tf2_ros::Buffer buffer; tf2_ros::TransformListener listener(buffer); // 5.处理订阅到的 TF
// 需要创建发布 /turtle2/cmd_vel 的 publisher 对象
ros::Publisher pub = nh.advertise<geometry_msgs::Twist>("/turtle2/cmd_vel",1000);
ros::Rate rate(10); while (ros::ok()) { try { //5-1.先获取 turtle1 相对 turtle2 的坐标信息 geometry_msgs::TransformStamped tfs = buffer.lookupTransform("turtle2","turtle1",ros::Time(0));
//5-2.根据坐标信息生成速度信息 -- geometry_msgs/Twist.h geometry_msgs::Twist twist; twist.linear.x = 0.5 * sqrt(pow(tfs.transform.translation.x,2) + pow(tfs.transform.translation.y,2)); twist.angular.z = 4 * atan2(tfs.transform.translation.y,tfs.transform.translation.x);
//5-3.发布速度信息 -- 需要提前创建 publish 对象 pub.publish(twist); } catch(const std::exception& e) { // std::cerr << e.what() << '\n'; ROS_INFO("错误提示:%s",e.what()); }
rate.sleep(); // 6.spin ros::spinOnce(); }
return 0; } |
配置文件此处略。
5.运行
使用 launch 文件组织需要运行的节点,内容示例如下:
HTML <!-- tf2 实现小乌龟跟随案例 --> <launch> <!-- 启动乌龟节点与键盘控制节点 --> <node pkg="turtlesim" type="turtlesim_node" name="turtle1" output="screen" /> <node pkg="turtlesim" type="turtle_teleop_key" name="key_control" output="screen"/> <!-- 启动创建第二只乌龟的节点 --> <node pkg="demo_tf2_test" type="Test01_Create_Turtle2" name="turtle2" output="screen" /> <!-- 启动两个坐标发布节点 --> <node pkg="demo_tf2_test" type="Test02_TF2_Caster" name="caster1" output="screen" args="turtle1" /> <node pkg="demo_tf2_test" type="Test02_TF2_Caster" name="caster2" output="screen" args="turtle2" /> <!-- 启动坐标转换节点 --> <node pkg="demo_tf2_test" type="Test03_TF2_Listener" name="listener" output="screen" /> </launch> |
5.1.7 TF2与TF
1.TF2与TF比较_简介
TF2已经替换了TF,TF2是TF的超集,建议学习 TF2 而非 TF
TF2 功能包的增强了内聚性,TF 与 TF2 所依赖的功能包是不同的,TF 对应的是tf包,TF2 对应的是tf2和tf2_ros包,在 TF2 中不同类型的 API 实现做了分包处理。
TF2 实现效率更高,比如在:TF2 的静态坐标实现、TF2 坐标变换监听器中的 Buffer 实现等
2.TF2与TF比较_静态坐标变换演示
接下来,我们通过静态坐标变换来演示TF2的实现效率。
2.1启动 TF2 与 TF 两个版本的静态坐标变换
TF2 版静态坐标变换:rosrun tf2_ros static_transform_publisher 0 0 0 0 0 0 /base_link /laser
TF 版静态坐标变换:rosrun tf static_transform_publisher 0 0 0 0 0 0 /base_link /laser 100
会发现,TF 版本的启动中最后多一个参数,该参数是指定发布频率
2.2运行结果比对
使用rostopic查看话题,包含/tf与/tf_static, 前者是 TF 发布的话题,后者是 TF2 发布的话题,分别调用命令打印二者的话题消息
rostopic echo /tf: 当前会循环输出坐标系信息
rostopic echo /tf_static: 坐标系信息只有一次
2.3结论
如果是静态坐标转换,那么不同坐标系之间的相对状态是固定的,既然是固定的,那么没有必要重复发布坐标系的转换消息,很显然的,tf2 实现较之于 tf 更为高效
5.2 rosbag
机器人传感器获取到的信息,有时我们可能需要时时处理,有时可能只是采集数据,事后分析,比如:
机器人导航实现中,可能需要绘制导航所需的全局地图,地图绘制实现,有两种方式,方式1:可以控制机器人运动,将机器人传感器感知到的数据时时处理,生成地图信息。方式2:同样是控制机器人运动,将机器人传感器感知到的数据留存,事后,再重新读取数据,生成地图信息。两种方式比较,显然方式2使用上更为灵活方便。
在ROS中关于数据的留存以及读取实现,提供了专门的工具: rosbag。
概念
是用于录制和回放 ROS 主题的一个工具集。
作用
实现了数据的复用,方便调试、测试。
本质
rosbag本质也是ros的节点,当录制时,rosbag是一个订阅节点,可以订阅话题消息并将订阅到的数据写入磁盘文件;当重放时,rosbag是一个发布节点,可以读取磁盘文件,发布文件中的话题消息。
另请参考:
5.2.1 rosbag使用_命令行
需求:
ROS 内置的乌龟案例并操作,操作过程中使用 rosbag 录制,录制结束后,实现重放
实现:
1.准备
创建目录保存录制的文件
Plaintext mkdir ./xxx cd xxx |
2.开始录制
Plaintext rosbag record -a -O 目标文件 |
操作小乌龟一段时间,结束录制使用 ctrl + c,在创建的目录中会生成bag文件。
3.查看文件
4.回放文件
重启乌龟节点,会发现,乌龟按照录制时的轨迹运动。
另请参考:
5.2.2 rosbag使用_编码
命令实现不够灵活,可以使用编码的方式,增强录制与回放的灵活性,本节将通过简单的读写实现演示rosbag的编码实现。
C++实现
1.写 bag
C++ #include "ros/ros.h" #include "rosbag/bag.h" #include "std_msgs/String.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"bag_write"); ros::NodeHandle nh; //创建bag对象 rosbag::Bag bag; //打开 bag.open("/home/rosdemo/demo/test.bag",rosbag::BagMode::Write); //写 std_msgs::String msg; msg.data = "hello world"; bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); bag.write("/chatter",ros::Time::now(),msg); //关闭 bag.close();
return 0; } |
2.读bag
C++ /* 读取 bag 文件:
*/ #include "ros/ros.h" #include "rosbag/bag.h" #include "rosbag/view.h" #include "std_msgs/String.h" #include "std_msgs/Int32.h"
int main(int argc, char *argv[]) {
setlocale(LC_ALL,"");
ros::init(argc,argv,"bag_read"); ros::NodeHandle nh;
//创建 bag 对象 rosbag::Bag bag; //打开 bag 文件 bag.open("/home/rosdemo/demo/test.bag",rosbag::BagMode::Read); //读数据 for (rosbag::MessageInstance const m : rosbag::View(bag)) { std_msgs::String::ConstPtr p = m.instantiate<std_msgs::String>(); if(p != nullptr){ ROS_INFO("读取的数据:%s",p->data.c_str()); } }
//关闭文件流 bag.close(); return 0; } |
5.3 rqt工具箱
之前,在 ROS 中使用了一些实用的工具,比如: ros_bag 用于录制与回放、tf2_tools 可以生成 TF 树 ..... 这些工具大大提高了开发的便利性,但是也存在一些问题: 这些工具的启动和使用过程中涉及到一些命令操作,应用起来不够方便,在ROS中,提供了rqt工具箱,在调用工具时以图形化操作代替了命令操作,应用更便利,提高了操作效率,优化了用户体验。
概念
ROS基于 QT 框架,针对机器人开发提供了一系列可视化的工具,这些工具的集合就是rqt
作用
可以方便的实现 ROS 可视化调试,并且在同一窗口中打开多个部件,提高开发效率,优化用户体验。
组成
rqt 工具箱组成有三大部分
- rqt_common_plugins——rqt 中常用的工具套件
- rqt_robot_plugins——运行中和机器人交互的插件(比如: rviz)
另请参考:
5.3.1 rqt安装启动与基本使用
1.安装
- 一般只要你安装的是desktop-full版本就会自带工具箱
Plaintext $ sudo apt-get install ros-noetic-rqt $ sudo apt-get install ros-noetic-rqt-common-plugins |
2.启动
rqt的启动方式有两种:
- 方式2:rosrun rqt_gui rqt_gui
3.基本使用
启动 rqt 之后,可以通过 plugins 添加所需的插件

5.3.2 rqt常用插件:rqt_graph
简介:可视化显示计算图
启动:可以在 rqt 的 plugins 中添加,或者使用rqt_graph启动

5.3.3 rqt常用插件:rqt_console
简介:rqt_console 是 ROS 中用于显示和过滤日志的图形化插件
准备:编写 Node 节点输出各个级别的日志信息
C++ /* ROS 节点:输出各种级别的日志信息
*/ #include "ros/ros.h"
int main(int argc, char *argv[]) { ros::init(argc,argv,"log_demo"); ros::NodeHandle nh;
ros::Rate r(0.3); while (ros::ok()) { ROS_DEBUG("Debug message d"); ROS_INFO("Info message oooooooooooooo"); ROS_WARN("Warn message wwwww"); ROS_ERROR("Erroe message EEEEEEEEEEEEEEEEEEEE"); ROS_FATAL("Fatal message FFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); r.sleep(); }
return 0; } |
启动:
可以在 rqt 的 plugins 中添加,或者使用rqt_console启动

5.3.4 rqt常用插件:rqt_plot
简介:图形绘制插件,可以以 2D 绘图的方式绘制发布在 topic 上的数据
准备:启动 turtlesim 乌龟节点与键盘控制节点,通过 rqt_plot 获取乌龟位姿
启动:可以在 rqt 的 plugins 中添加,或者使用rqt_plot启动

5.3.5 rqt常用插件:rqt_bag
简介:录制和重放 bag 文件的图形化插件
准备:启动 turtlesim 乌龟节点与键盘控制节点
启动:可以在 rqt 的 plugins 中添加,或者使用rqt_bag启动
录制:

重放:

第 6 章 ros仿真和slam
待更新