ROS2 Humble学习笔记 (2)

本文发表于个人的github pages。因csdn本身显示插件和转载过程中导致显示不太友好。建议大家阅读原文。
想查看完整内容,请移步到ROS2 Humble学习笔记2

本文篇幅较长,可抽空按照章节阅读。本文只作为对入门教程的一种浮现和提升。

一、前言

在上一篇学习笔记中,我们学习ROS2的一些基本概念,主要是官方入门教程中的Beginner: CLI tools部分。现在我们继续学习Beginner: Client libraries.这部分将设计到ROS2的编程和代码等部分。通过这一部分学习,也能够对于ROS2的一些基本概念有更深入的理解。

二、ROS2编程基础

这篇笔记将记录在学习官方入门教程Beginner: Client libraries部分的具体内容和尝试遇到的问题,以及一些额外的思考。

2.1 Colcon入门

本小节内容主要参考Colcon Tutorial和A universal build tool。

2.1.1 Colcon的设计原则

在 ROS 生态系统中,软件被分成许多软件包。开发人员同时开发多个软件包的情况非常普遍。这与工作流程形成了鲜明对比,在工作流程中,开发人员一次只开发一个软件包,所有依赖关系都是一次性提供的,而不是不断迭代。

“手动”构建软件包的方法包括按照拓扑顺序逐个构建所有软件包。对于每个软件包,文档通常都会说明依赖关系是什么、如何设置环境来构建软件包,以及之后如何设置环境来使用软件包。如果没有一个能自动完成这一过程的工具,这样的工作流程在大规模的情况下是不可行的。

因此,ROS2的设计者就希望设计一个统一构建(build)工具,通过一次调用完成一组软件包的构建。它应当同时支持ROS1(向后兼容)和ROS2的软件包的构建。如果必要的元信息可以通过推断和/或外部提供的方式获得,那么它还能与那些本身不提供清单文件的软件包协同工作。这样,构建工具就能用于非 ROS 软件包(如 Gazebo,包括其点火依赖项、sdformat 等)。

尽管在ROS的生态系统中,有几种工具满足上述要求。大多都大同小异,因为是单独开发的,很多时候某些必要功能只存在与某个构建工具中。这就是为什么ROS2的设计者希望一个功能完善的统一构建工具的原因。想一想Python中数量繁多的包管理和依赖解决工具带来的后果。另外作者也提到了这样一个问题,这确实更能说明他们希望的构建工具到底是什么:

由于本文的重点是构建工具,因此需要澄清与构建系统的区别。

编译工具(Build Tool)对一组软件包进行操作。它确定依赖关系图,并按拓扑顺序为每个软件包调用特定的构建系统。构建工具本身应尽可能少地了解特定软件包所使用的构建系统。只需知道如何为其设置环境、调用构建和设置环境以使用构建的软件包即可。现有的 ROS 构建工具包括:catkin_make、catkin_make_isolated、catkin_tools 和 ament_tools。

另一方面,编译系统(Build System)是在单个软件包上运行的。例如 Make、CMake、Python setuptools 或 Autotools(ROS 目前没有使用)。例如,CMake 软件包可以通过调用以下步骤来构建:cmake、make、make install。

另外作者强调了Colcon应当功能单一,总之尽可能符合软件开发原则:

  • 关注点分离
  • 单一职责原则
  • 最少知识原则
  • 不要重复自己
  • 保持愚蠢简单
  • “不为不使用的东西付费”

作者提到了ROS2上已经有专门的获取构建工具所需源码的工具(例如rosinstall 或 wstool(对于 .rosinstall 文件)或 vcstool(对于 .repos 文件)),也有专门的依赖项安装工具(rosdep),二进制包生成工具(如bloom等)。

2.1.2 Colcon介绍

Colcon是一个构建工具,可以用来构建ROS2项目。它可以帮助我们更加方便地管理ROS2项目,包括编译、测试、安装等

2.1.3 安装Colcon

通常ros2-desktop中已经安装好了colcon.但是如果你要单独安装(这里只关注我目前使用的Ubuntu22.04),则可以使用apt安装:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>sudo apt install python3-colcon-common-extensions
</code></span></span></span></span>

另外,我也看到有些包,比如MoveIt 2使用了colcon的mixin扩展。(colcon-mixin是colcon-core 的扩展,用于从存储库获取和管理 CLI mixins。)

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ sudo apt install python3-colcon-mixin
$ colcon mixin add default https://raw.githubusercontent.com/colcon/colcon-mixin-repository/master/index.yaml
$ colcon mixin update default
</code></span></span></span></span>

(注:请注意当需要显示终端回应的内容时,我会在用户输入的命令前添加`$`符号,以表示命令提示符。以下不再赘述。)

2.1.4 Colcon的目录结构

当新创建一个colcon软件包时,先要在其内部创建一个名为src的子文件夹用以存放代码。 这里假定我们创建一个软件包的工作空间叫做demo_ws.我们可以这样做:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>mkdir <span style="color:#f4bf75">-p</span> demo_ws/src
cd demo_ws
</code></span></span></span></span>

这样我们就创建一个名字叫demo_ws的工作空间,并在其内部创建一个名为src的子文件夹。 因为本节的目的是学习colcon的使用,所以我们可以先从官方提供的examples中clone代码。这个仓库里面有针对不同ROS2版本的example, 所以用户可以选择针对自己的ROS2版本进行clone。当然记得将clone的代码放在src文件夹中。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>git clone https://github.com/ros2/examples src/examples <span style="color:#f4bf75">-b</span> humble
</code></span></span></span></span>

然后我们不妨熟悉一下examples的目录结构:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ls src/examples/
CONTRIBUTING.md  launch_testing  LICENSE  rclcpp  rclpy  README.md

<span style="color:#888888">## 当然也可以用目录树的结构展示</span>
$ tree . <span style="color:#f4bf75">-L</span> 3
.
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── launch_testing
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

5 directories, 3 files
</code></span></span></span></span>

相比于入门教程多了一个文件夹:launch_testing。这个文件夹包含launch和launch_testing 包的简单用例。这些旨在帮助初学者开始使用这些软件包并帮助他们理解这些概念。 而rclcpp和rclpy分别是C++和python相关的示例代码。

当colcon完全编译之后,它内部的文件夹结构如下:

.
├── build
├── install
├── log
└── src

src就是我们刚才将源码放入的目录;build是编译空间;install是安装空间;log是调试或者编译的记录。

2.1.5 underlay和overlay

还记得在Beginner:CLI中,每次启动turtlesim的时候都要source一下/opt/ros/humble/setup.bash了吗?那里会配置我们启动turtlesim所需的各种依赖和环境变量。

在使用colcon编译的时候同样需要我们借助setup script(设置脚本)来创建一个包含示例软件包所需的构建依赖项的工作区。我们称这种环境为 underlay(底层环境)。因为underlay似乎没有好的翻译,但大概可以翻译叫做基础环境或者底层环境。表示上层或中层的代码编译、执行、测试都有需要依赖于它。后面的描述中我们就直接叫做underlay.

现在我们的工作区demo_ws将是现有ROS2安装的overlay(覆盖层)。overlay这个概念如果生硬的翻译就是“覆盖环境”。这是相对于underlay这个概念而言的。通常来说,当你迭代少量的packages时,建议使用一个独立的overlay,而不是将所有的packages放在同一个工作区中。

2.1.6 Build the Worksapce/工作区编译

catkin中除了代码空间/src(source space)、编译空间/build(build space)、安装空间/install(install space)。还有专门的devel(development space)用来存放编译生成的可执行文件等。但是新的ament_cmake不支持devel,需要安装包。这时候可以在build的时候--symlink-install选项,这样可以要求编译器尽可能使用符号链接而不是复制文件。这样就可以更改源代码中的配置文件来更改已安装的文件,从而加速项目的迭代。(这一部分解释还不是很理解)

终于来到激动人心的编译环节了:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 请确保当前正位于demo_ws目录下</span>
colcon build <span style="color:#f4bf75">--symlink-install</span>
</code></span></span></span></span>

编译完成会有形如“Summary: {x} packages finished [{n}s]”这样的字样。信息比较直观,不再赘述。我们可以查看现在的文件目录:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ tree <span style="color:#f4bf75">-L</span> 1
.
├── build
├── install
├── log
└── src

4 directories, 0 files
</code></span></span></span></span>

这个目录结构在前面已经简单介绍过了。我们可以深入的浏览一下每个目录的文件,以加深印象。

另外,如果您不想构建特定的软件包,将一个名为COLCON_IGNORE的空文件放在目录中,则不会索引。你还可以在build,install,log的目录中发现这个文件。我推测这个文件相当于一个标签,编译器会忽略索引这个文件所在的目录。

2.1.7 test/测试

colcon的功能十分强大,因此命令也就异常复杂。慢慢了解吧。我们先来看看如果使用colcon来测试。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>colcon test
</code></span></span></span></span>

测试完成之后也会有类似的提示:“Summary: {x} packages finished [{n}s]”。 中间还可能有一些警告,比如下图: 

colcon test demo

图1:colcon test结果示例图

如果要避免在CMAKE软件包中配置和建造测试,则可以通过:-CMAKE -ARGS -DBUILD_TESTING = 0。

如果你想运行特定的测试,可以使用如下命令:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>colcon test <span style="color:#f4bf75">--packages-select</span> <package_name> <span style="color:#f4bf75">--ctest-args</span> <span style="color:#f4bf75">-R</span> <YOUR_TEST_IN_PKG>
</code></span></span></span></span>
2.1.8 setup/设置

在进一步测试之前,需要source一下生成的setup脚本,才能为新生成的package执行包创建包含必须依赖的工作空间。做法和之前创建underlay的工作空间一样。因为ubuntu的terminal是bash,以后就不强调这一点。如果你的是其它的terminal,你还可以选择使用ps1,sh,zsh等。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>source install/setup.bash
</code></span></span></span></span>
2.1.9 try/尝试

现在我们来尝试一下example里面的demo.入门教程里面演示的是examples_rclcpp_minimal_subscriber和examples_rclcpp_minimal_publisher这一组examples.打开两个终端窗口,一个担任subscriber一个担任publisher. 

rclcpp minimal demo

图2:rclcpp minimal demo

2.1.10 create an package/新包

colcon每个包都有一个package.xml文件,此文件定义了作者、版本、依赖等信息。我们不妨打开一个examples_rclcpp_minimal_publisher的package.xml文件,并使用xmltool工具解析一下它的构成。 

package_xml_parse

图3:package.xml解析结果

上图很清晰的展示了xml的主要节点。我们可以看到此包的build、execute和test都依赖rclcpp和std_msgs。也可以看到编译类型是ament_cmake

colcon支持多种构建类型。推荐的类型是ament_cmake和ament_python。也支持纯cmake包。 ament_cmake是C/c++的构建类型。ament_python则是python的构建类型。

我们可以使用ros2 pkg create去创建基于模板的新包。现在来尝试一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ cd src/examples/rclcpp

$ ros2 pkg create <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--dependencies</span> rclcpp std_msgs <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"It is an demo package"</span>  <span style="color:#f4bf75">--license</span> MIT  demo_pkg
going to create a new package
package name: demo_pkg
destination directory: /home/galileo/Workspaces/ROS2/execises/demo_ws/src/examples/rclcpp
package format: 3
version: 0.0.0
description: It is an demo package
maintainer: <span style="color:#d0d0d0">[</span><span style="color:#90a959">'galileo <zjh.2008.09@gmail.com>'</span><span style="color:#d0d0d0">]</span>
licenses: <span style="color:#d0d0d0">[</span><span style="color:#90a959">'MIT'</span><span style="color:#d0d0d0">]</span>
build type: ament_cmake
dependencies: <span style="color:#d0d0d0">[</span><span style="color:#90a959">'rclcpp'</span>, <span style="color:#90a959">'std_msgs'</span><span style="color:#d0d0d0">]</span>
creating folder ./demo_pkg
creating ./demo_pkg/package.xml
creating source and include folder
creating folder ./demo_pkg/src
creating folder ./demo_pkg/include/demo_pkg
creating ./demo_pkg/CMakeLists.txt

$ tree demo_pkg/
demo_pkg/
├── CMakeLists.txt
├── include
│   └── demo_pkg
├── LICENSE
├── package.xml
└── src

3 directories, 3 files

</code></span></span></span></span>

这样我们就新建了一个包,只是里面暂时没有代码。关于ros2 pkg create的详细用法,你可以使用ros2 pkg create -h去仔细查看。请尽量选择设置一个license,否则里面可能会产生警告提示。

2.1.11 colcon_cd

ROS2还提供一个快速跳转的工具,但是默认是没有生效的。所以需要提前设置一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>echo <span style="color:#90a959">"source /usr/share/colcon_cd/function/colcon_cd.sh"</span> <span style="color:#d0d0d0">>></span> ~/.bashrc
echo <span style="color:#90a959">"export _colcon_cd_root=/opt/ros/humble/"</span> <span style="color:#d0d0d0">>></span> ~/.bashrc

<span style="color:#888888">## 如果有必要可以查看一下添加是否成功</span>
cat ~/.bashrc | grep colcon_cd

</code></span></span></span></span>

为了让新添加的生效,我们可以重新打开一下shell.我们可以检验一下是否成功。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ colcon_cd rclcpp
$ pwd
/opt/ros/humble/share/rclcpp
$ colcon_cd std_msgs
$ pwd
/opt/ros/humble/share/std_msgs
$ colcon_cd examples_rclcpp_minimal_publisher
$ pwd
/opt/ros/humble/share/examples_rclcpp_minimal_publisher
</code></span></span></span></span>

可以看出这个工具确实挺方便的,但是前提是你需要知道包的正确名称。另外如果没有source过的包,这个工具也无法跳转。比如,我们尝试寻找刚才的demo_pkg

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ colcon_cd demo_pkg
Could neither find package <span style="color:#90a959">'demo_pkg'</span> from <span style="color:#90a959">'/opt/ros/humble/'</span> nor from the current working directory
</code></span></span></span></span>
2.1.12 colcon命令自动补全

colcon支持命令自动补全,但是默认是没有开启的。如果需要开启,需要在bashrc中添加一行:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>echo <span style="color:#90a959">"source /usr/share/colcon_argcomplete/hook/colcon-argcomplete.bash"</span> <span style="color:#d0d0d0">>></span> ~/.bashrc
<span style="color:#888888">## 如果有必要可以查看一下添加是否成功</span>
cat ~/.bashrc | grep argcomplete
</code></span></span></span></span>

然后重新打开shell,就可以使用命令自动补全了。

2.2 Workspace/工作区

本小节内容主要参考creating a workspace tutorial和A universal build tool。

我们在上一小节介绍了underlay和overlay的概念。工作区(Workspace)就是一个包含ROS包的目录。我们每次在启动ROS2的时候都要Source一下(如果你将source的代码放在.bashrc中,则不需要每次都手动source),这个过程实际就是在配置必要软件包的工作区。

2.2.1 Creating a workspace/创建工作区

按照官方教程创建名字工作区。这部分的概念和步骤和上一小节一致。不再赘述。这一节的不同点在于我们创建的工作区名称叫做demo2_ws.(原文叫做ros2_ws,我这里改为demo2_ws是为了和上一小节做区分。) 另外这一节的样例代码变成了ros tutories。首先用cd指令跳转到你的目标目录。然后执行以下命令:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>mkdir <span style="color:#f4bf75">-p</span> demo2_ws/src
cd demo2_ws/src
git clone https://github.com/ros/ros_tutorials.git <span style="color:#f4bf75">-b</span> humble
</code></span></span></span></span>

如果成功clone,我们照例使用tree命令查看一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ tree <span style="color:#f4bf75">-L</span> 2
.
└── ros_tutorials
    ├── roscpp_tutorials
    ├── rospy_tutorials
    ├── ros_tutorials
    └── turtlesim
</code></span></span></span></span>
2.2.2 Resolve dependencies/依赖关系

这一节主要关注ROS2包的依赖关系。在我们编写好代码或者copy示例程序之后,在编译之前,我们最好先解决依赖关系。不然当你花费和很久时间才发现缺少必要的依赖项,这将非常的不划算。

我们可以用rosdep命令来解决依赖关系。当然我们首先要回到我们的工作区目录,然后使用rosdep来执行依赖检查。如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 因为我们刚才在src目录,现在需要回到工作区根目录</span>
$ cd ../
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
</code></span></span></span></span>

上面的命令稍微有些复杂,我们可以先学习一下rosdep命令和rosdep install。如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep 
Usage: rosdep <span style="color:#d0d0d0">[</span>options] <command<span style="color:#d0d0d0">></span> <args>

Commands:

rosdep check <stacks-and-packages>...
  check <span style="color:#aa759f">if </span>the dependencies of package<span style="color:#d0d0d0">(</span>s<span style="color:#d0d0d0">)</span> have been met.
<span style="color:#888888">## rosdep check检查依赖项是否都满足,我的理解是应该使用这个命令检查依赖关系。</span>

rosdep install <stacks-and-packages>...
  download and install the dependencies of a given package or packages.
<span style="color:#888888">## rosdep install应该是用来下载依赖项。但是入门教程使用这个去检查并自动下载依赖。</span>

rosdep db
  generate the dependency database and print it to the console.
<span style="color:#888888">## rosdep db命令生成依赖数据库,并打印到控制台。</span>

rosdep init
  initialize rosdep sources <span style="color:#aa759f">in</span> /etc/ros/rosdep.  May require sudo.
<span style="color:#888888">## rosdep init命令初始化rosdep源。可能需要以root权限运行。</span>

rosdep keys <stacks-and-packages>...
  list the rosdep keys that the packages depend on.

rosdep resolve <rosdeps>
  resolve <rosdeps> to system dependencies

rosdep update
  update the local rosdep database based on the rosdep sources.

rosdep what-needs <rosdeps>...
  print a list of packages that declare a rosdep on <span style="color:#d0d0d0">(</span>at least
  one of<span style="color:#d0d0d0">)</span> <rosdeps>

rosdep where-defined <rosdeps>...
  print a list of yaml files that declare a rosdep on <span style="color:#d0d0d0">(</span>at least
  one of<span style="color:#d0d0d0">)</span> <rosdeps>

rosdep fix-permissions
  Recursively change the permissions of the user<span style="color:#90a959">'s ros home directory.
  May require sudo.  Can be useful to fix permissions after calling
  "rosdep update" with sudo accidentally.


rosdep: error: Please enter a command
</span></code></span></span></span></span>

我按照自己的理解尝试使用rosdep check来检查,也成功了:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep check <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--rosdistro</span> humble
All system dependencies have been satisfied
</code></span></span></span></span>

要想查看rosdep checkrosdep install的详细信息,可以使用-h选项。因为命令较多,就不一一解释。这里只关注这次使用的这几个选项的含义:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-h</span>
<span style="color:#888888">## 只摘录部分内容</span>
<span style="color:#f4bf75">--os</span><span style="color:#d0d0d0">=</span>OS_NAME:OS_VERSION
                        Override OS name and version <span style="color:#d0d0d0">(</span>colon-separated<span style="color:#d0d0d0">)</span>, e.g.
                        ubuntu:lucid
<span style="color:#f4bf75">-c</span> SOURCES_CACHE_DIR, <span style="color:#f4bf75">--sources-cache-dir</span><span style="color:#d0d0d0">=</span>SOURCES_CACHE_DIR
                    Override /home/galileo/.ros/rosdep/sources.cache
<span style="color:#f4bf75">-y</span>, <span style="color:#f4bf75">--default-yes</span>     Tell the package manager to default to y or fail when
<span style="color:#f4bf75">-i</span>, <span style="color:#f4bf75">--ignore-packages-from-source</span>, <span style="color:#f4bf75">--ignore-src</span>
                        Affects the <span style="color:#90a959">'check'</span>, <span style="color:#90a959">'install'</span>, and <span style="color:#90a959">'keys'</span> verbs. If
                        specified <span style="color:#aa759f">then </span>rosdep will ignore keys that are found
                        to be catkin or ament packages anywhere <span style="color:#aa759f">in </span>the
                        ROS_PACKAGE_PATH, AMENT_PREFIX_PATH or <span style="color:#aa759f">in </span>any of the
                        directories given by the <span style="color:#f4bf75">--from-paths</span> option.

<span style="color:#f4bf75">--from-paths</span>          Affects the <span style="color:#90a959">'check'</span>, <span style="color:#90a959">'keys'</span>, and <span style="color:#90a959">'install'</span> verbs. If
                    specified the arguments to those verbs will be
                    considered paths to be searched, acting on all catkin
                    packages found there <span style="color:#aa759f">in</span>.
<span style="color:#f4bf75">--rosdistro</span><span style="color:#d0d0d0">=</span>ROS_DISTRO
                        Explicitly sets the ROS distro to use, overriding the
                        normal method of detecting the ROS distro using the
                        ROS_DISTRO environment variable. When used with the
                        <span style="color:#90a959">'update'</span> verb, only the specified distro will be
                        updated.
</code></span></span></span></span>

-i选项将会忽略在 ROS_PACKAGE_PATH、AMENT_PREFIX_PATH 或 –from-paths 选项指定的任何目录中的任意位置发现的 catkin 或 ament 包的键。

--from-paths用来搜索这个路径下所有的catkin软件包。

--rosdistro用来制定ROS的版本,比如我们用的humble.

-y告诉软件管理器默认为yes.

入门教程和介绍了从source或者fat archive的安装。参数更加复杂。这里不再赘述。(因为我也没有尝试)

总之如果依赖全部already,会提示#All required rosdeps installed successfully

包是通过package.xml文件来声明依赖项的。后面会详细介绍。在2.1.10其实也简单提到过。所以清晰的文档结构也帮助rosdep来快速的检查依赖关系。

2.2.3 编译

这一章和2.1中的步骤没有什么特殊。不再赘述:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ colcon build
<span style="color:#888888">## 省略输出内容</span>
Summary: 1 package finished <span style="color:#d0d0d0">[</span>14.8s]

$ tree <span style="color:#f4bf75">-L</span> 2
.
├── build
│   ├── COLCON_IGNORE
│   └── turtlesim
├── install
│   ├── COLCON_IGNORE
│   ├── local_setup.bash
│   ├── local_setup.ps1
│   ├── local_setup.sh
│   ├── _local_setup_util_ps1.py
│   ├── _local_setup_util_sh.py
│   ├── local_setup.zsh
│   ├── setup.bash
│   ├── setup.ps1
│   ├── setup.sh
│   ├── setup.zsh
│   └── turtlesim
├── log
│   ├── build_2024-01-19_20-14-45
│   ├── COLCON_IGNORE
│   ├── latest -> latest_build
│   └── latest_build -> build_2024-01-19_20-14-45
└── src
    └── ros_tutorials

10 directories, 13 files
</code></span></span></span></span>

有意思的是如果你观察src/ros_tutorials目录,里面有好几个文件夹。但是最终生成的只有一个package.而上一章其实生成了好几个packages.这一点可以留个疑问。

入门教程还对几个参数做了解释:

  • –packages-up-to(构建你想要的软件包及其所有依赖包),但不构建整个工作区(节省时间
  • –symlink-install让你在每次修改 python 脚本时都不必重新构建。
  • –event-handlers console_direct+ 在构建时显示控制台输出(也可以在日志目录中找到)。
2.2.4 运行测试

要运行测试,老样子还是要source一下underlay和新建的包(overlay)。如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 教程又提了一遍,但是如果你已经将这个命令写入到.bashrc就没必要重复</span>
source /opt/ros/humble/setup.bash

<span style="color:#888888">## 进入demo2_ws工作区,否则不能完成。我这里已经ready.就不再执行。</span>
<span style="color:#888888">## 这里使用的是local_setup,为什么没用setup。下文有介绍</span>
source install/local_setup.bash
</code></span></span></span></span>

这里需要说明一下local_setupsetup的区别:

  • local_setup是ROS2的本地设置,即只设置overlay的工作环境。因为我们之前source了/opt/ros/humble/setup.bash相当于手动source了underlay
  • setup不仅会设置overlay的工作环境还会设置underlay的工作环境。所以也可以只用一步setup让两个工作区都ready. 测试命令之前也用过:
    <span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>ros2 run turtlesim turtlesim_node
    </code></span></span></span>

    但是我们怎么知道这个是overlay的,而不是underlay的呢?因为即便我们没有使用local_setup也可以运行.

2.2.5 修改测试

为了验证确实是我们的overlay运行了,最简单的办法是修改一下窗口的标题或者窗口的大小等信息。 我这里使用vscode去打开整个工作区。src/ros_tutorials下面有4个目录。经过分析之后感觉目标目录应该就是“turtlesim/src”下面的文件。 入门教程里面提到要修改ros_tutorials/turtlesim/src/turtle_frame.cpp和我的查找是一致的。我们就开始修改吧: 

第一次代码修改

图4:代码修改

可以看到代码只要做了三个大的修改:

  1. 修改了背景颜色(DEFAULT_BG_R,DEFAULT_BG_G,DEFAULT_BG_B三个地方)
  2. 窗口尺寸由(500, 500)改为了(600, 600)
  3. 标题由”TurtleSim”改为了”WuguiSim”

修改完成之后:根据步骤前面已经介绍的方法,完成编译。然后我们来设法做一个对比。一个启用了overlay(即使用source install/local_setup.bash),另一个不用。效果演示如下: 

两个不同的turtlesim

图5:两个不同的turtlesim界面

  • 第一个窗口背景为艳粉色的是Overlay,可以看到窗口的title也更改为了”WuguiSim”,窗口也比第二个大了很多。(我屏幕分辨率比较高,所以窗口显示的可能比你电脑上的小一些。)
  • 第二个窗口和我们之前测试的一样。窗口明显比第一个小了一圈。标题还是TurtleSim。

所以我们可以认为Overlay层是先被寻找的。类似与C语言的局部变量。当在Overlay里面找不到我们需要的package的时候才会去修找underlay层的包。如果启用了我们修改过的Overlay,因为turtlesim已经在这里寻找到了。所以就会出现我们做出修改的窗口。

2.3 ROS2 Package和它的创建

奇怪的是,我其实在笔记1中也没有自己提过Package是什么。官方其实也没有将。我们其实之前用ROS2 pack这个命令对包进行过一番操作。因为software package默认是一个大家都熟知的概念。但是其实我自己并不能给它一个很好的解释。我们不妨来看看官方怎么解释的吧:

A package is an organizational unit for your ROS 2 code. If you want to be able to install your code or share it with others, then you’ll need it organized in a package. With packages, you can release your ROS 2 work and allow others to build and use it easily.

(对于ROS2来说)一个软件包(Package,简称软件包)是ROS2的代码管理单元。如果你想要安装你的的代码,或者将它们分享给其他人,你需要将它们组织成一个包的形式。通过包,您可以发布您的ROS2作品并允许其他人轻松构建和使用它。

这个概念其实很清晰。首先它是软件代码的组织形式,通过软件包这种形式会将代码中的所有文件见按照某种形式组织/捆绑在一起。第二,你发布给别人的时候,也是将你的代码当作一个整体(package)发布出去。别人获取时也是b把这个包的整体获取过来。否则软件将事实不完整的。

ROS2中的包创建使用ament作为其构建系统,并使用colcon作为其构建工具。您可以使用官方支持的CMake或Python创建包,但也存在其他构建类型。

2.3.1 ament

(注:这部分主要参考about build system这一篇的内容。)

ament是ROS2的构建系统。它是ROS2的核心组件之一。ament的主要目的是帮助ROS2项目开发者快速、可靠的构建ROS2软件。我们不妨看一下官方是如何解释ament的:

Under everything is the build system. Iterating on catkin from ROS 1, we have created a set of packages under the moniker ament. Some of the reasons for changing the name to ament are that we wanted it to not collide with catkin (in case we want to mix them at some point) and to prevent confusion with existing catkin documentation. ament’s primary responsibility is to make it easier to develop and maintain ROS 2 core packages. However, this responsibility extends to any user who is willing to make use of our build system conventions and tools. Additionally it should make packages conventional, such that developers should be able to pick up any ament based package and make some assumptions about how it works, how to introspect it, and how to build or use it.

一切之下都是构建系统。 在 ROS 1 的 catkin 上进行迭代,我们创建了一组名为 ament 的包。 将名称更改为 ament 的一些原因是我们希望它不与 catkin 冲突(以防我们想在某个时候将它们混合)并防止与现有的 catkin 文档混淆。 ament 的主要职责是让 ROS 2 核心包的开发和维护变得更加容易。 然而,这一责任延伸到任何愿意使用我们的构建系统约定和工具的用户。 此外,它应该使包变得常规,这样开发人员应该能够选择任何基于 ament 的包,并对其如何工作、如何内省以及如何构建或使用它做出一些假设。

ament是一个不断发展的构建系统,目前主要由ament_package,ament_cmake, ament_lint和build tools组成。它们被托管在ament的github仓库中。关于这几个仓库具体包含什么内容这里就不再赘述。下面只描述与之相关的一些概念。

  • ament packages :任何包含package.xml并遵循ament打包准则的包,无论底层构建系统如何。package.xml“清单”文件包含处理和操作包所需的信息。此包信息包括全局唯一的包名称以及包的依赖项等内容。package.xml文件还充当标记文件,指示包在文件系统上的位置。package.xml文件的解析由catkin_pkg提供(如 ROS 1 中所示),而通过在文件系统中搜索这些package.xml文件来定位包的功能由构建工具(例如 colcon)提供。

  • ament cmake pacakge :使用CMake构建的ament包,它遵循ament的打包准则。这种类型的包由 package.xml 文件的<export>标记中的 <build_type>ament_cmake</build_type> 标记标识。

  • ament Python package :遵循ament打包指南的Python包。

  • setuptools : 它是python常用的一个打包和分发工具。它也是ament中python package的打包工具。

  • package.xml :包的清单文件(manifest file)。标记包的根并包含有关包的元信息,包括其名称、版本、描述、维护者、许可证、依赖项等。 清单的内容采用机器可读的XML格式,并且内容在REP 127和140中描述,并且有可能在未来的REP中进一步修改。

2.3.2 Package的结构

我们首先来了解一下ament包的组成。对于cmake的包它包含这样几个关键文件/文件夹:

  • CMakeLists.txt 描述如何在包中构建代码
  • __include/__ 包含包的公共标头的目录
  • package.xml 文件包含了包的元信息
  • src 目录包含包的源代码

其实最简单的是我们去查看一下我们之前2.1中的包的结构:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ tree <span style="color:#f4bf75">-L</span> 3 wait_set/
wait_set/
├── CHANGELOG.rst
├── CMakeLists.txt
├── include
│   └── wait_set
│       ├── listener.hpp
│       ├── random_listener.hpp
│       ├── random_talker.hpp
│       ├── talker.hpp
│       └── visibility.h
├── package.xml
├── README.md
└── src
    ├── executor_random_order.cpp
    ├── listener.cpp
    ├── static_wait_set.cpp
    ├── talker.cpp
    ├── thread_safe_wait_set.cpp
    ├── wait_set_composed.cpp
    ├── wait_set.cpp
    ├── wait_set_random_order.cpp
    ├── wait_set_topics_and_timer.cpp
    └── wait_set_topics_with_different_rates.cpp

3 directories, 19 files
</code></span></span></span></span>

可以看出这个package包含了上面提到的四个部分。当然还有一些其它几个ament包不需要的文件:CHANGELOG.rstREADME.md等。

我们再来看看python的包组成:

  • package.xml 文件包含有关包的元信息
  • __resource/__ 是包的标记文件
  • setup.cfg 当包有可执行文件时需要setup.cfg,因此ros2 run可以找到它们
  • setup.py 包含如何安装包的说明
  • ____ 与您的包同名的目录,ROS2工具使用它来查找您的包,包含`__init__.py`

其实最简单的是我们去查看一下我们之前2.1中的包的结构:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ tree <span style="color:#f4bf75">-L</span> 3 minimal_publisher/
minimal_publisher/
├── CHANGELOG.rst
├── examples_rclpy_minimal_publisher
│   ├── __init__.py
│   ├── publisher_local_function.py
│   ├── publisher_member_function.py
│   ├── publisher_old_school.py
│   └── __pycache__
│       └── __init__.cpython-310.pyc
├── package.xml
├── README.md
├── resource
│   └── examples_rclpy_minimal_publisher
├── setup.cfg
├── setup.py
└── test
    ├── __pycache__
    │   ├── test_copyright.cpython-310-pytest-6.2.5.pyc
    │   ├── test_flake8.cpython-310-pytest-6.2.5.pyc
    │   └── test_pep257.cpython-310-pytest-6.2.5.pyc
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

5 directories, 17 files
</code></span></span></span></span>

可以看出这个package包含了上面提到的五个部分。当然还有一些其它几个ament包不需要的文件:CHANGELOG.rstREADME.md等。

比较Cmake和Python的ament包相似之处是都包含了package.xml文件。在Cmake中的include文件夹了里面包含了一个和包名相同的子文件,而Python的resource中也有一个和包名相同的标记文件。两个package.xml文件的build_type标签也不相同。分标包含了ament_cmake标签和ament_python标签。

然而比较让我觉得不理解的是下面这种结构。一个是rclcpp/topics/minimal_publisher/,它的结构如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ tree minimal_publisher/
minimal_publisher/
├── CHANGELOG.rst
├── CMakeLists.txt
├── lambda.cpp
├── member_function.cpp
├── member_function_with_type_adapter.cpp
├── member_function_with_unique_network_flow_endpoints.cpp
├── member_function_with_wait_for_all_acked.cpp
├── not_composable.cpp
├── package.xml
└── README.md

0 directories, 10 files
</code></span></span></span></span>

可以看到这个包根本没有include文件夹。和cmake ament的要求的包结构不一样。但应该也是合理的。后面再继续关注这个情况。

另外需要注意的是一个workspace可以包含一个或者多个package,这些package可以是python package或者cmake package.甚至其它受支持的构建系统。比如cargo ament(编译rust包)。但是需要注意它们不能相互嵌套。(一个包里面包含另一个包的情况是不允许的。它们应该都有独立的包结构。)下面是入门教程示例的一个简单的文件结构:

workspace_folder/
    src/
      cpp_package_1/
          CMakeLists.txt
          include/cpp_package_1/
          package.xml
          src/

      py_package_1/
          package.xml
          resource/py_package_1
          setup.cfg
          setup.py
          py_package_1/
      ...
      cpp_package_n/
          CMakeLists.txt
          include/cpp_package_n/
          package.xml
          src/

我们在2.1和2.2小节使用的目录结构都符合这种推荐的方式。

2.3.3 尝试自己创建一个Package

(这一部分的操作我们在2.1.10中其实已经简单尝试过。这里再操作一次是为了更加熟练和深入。) 我们现在先回到我们在2.2小节使用的那个工作区。(就是运行turtlesim的那个工作区) 这一小节,将使用ros2 pack create来创建package.以下的操作假定你已经跳转到了demo2_ws工作区。现在让我们开始吧:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg create <span style="color:#f4bf75">-h</span>
usage: ros2 pkg create <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">-h</span><span style="color:#d0d0d0">]</span> <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--package-format</span> <span style="color:#d0d0d0">{</span>2,3<span style="color:#d0d0d0">}]</span> <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--description</span> DESCRIPTION] <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--license</span> LICENSE]
                       <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--destination-directory</span> DESTINATION_DIRECTORY] <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--build-type</span> <span style="color:#d0d0d0">{</span>cmake,ament_cmake,ament_python<span style="color:#d0d0d0">}]</span>
                       <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--dependencies</span> DEPENDENCIES <span style="color:#d0d0d0">[</span>DEPENDENCIES ...]] <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--maintainer-email</span> MAINTAINER_EMAIL]
                       <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--maintainer-name</span> MAINTAINER_NAME] <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--node-name</span> NODE_NAME] <span style="color:#d0d0d0">[</span><span style="color:#f4bf75">--library-name</span> LIBRARY_NAME]
                       package_name

Create a new ROS 2 package

positional arguments:
  package_name          The package name

options:
  <span style="color:#f4bf75">-h</span>, <span style="color:#f4bf75">--help</span>            show this help message and exit
  <span style="color:#f4bf75">--package-format</span> <span style="color:#d0d0d0">{</span>2,3<span style="color:#d0d0d0">}</span>, <span style="color:#f4bf75">--package_format</span> <span style="color:#d0d0d0">{</span>2,3<span style="color:#d0d0d0">}</span>
                        The package.xml format.
  <span style="color:#f4bf75">--description</span> DESCRIPTION
                        The description given <span style="color:#aa759f">in </span>the package.xml
  <span style="color:#f4bf75">--license</span> LICENSE     The license attached to this package<span style="color:#d0d0d0">;</span> this can be an arbitrary string, but a LICENSE file will only be generated
                        <span style="color:#aa759f">if </span>it is one of the supported licenses <span style="color:#d0d0d0">(</span>pass <span style="color:#90a959">'?'</span> to get a list<span style="color:#d0d0d0">)</span>
  <span style="color:#f4bf75">--destination-directory</span> DESTINATION_DIRECTORY
                        Directory where to create the package directory
  <span style="color:#f4bf75">--build-type</span> <span style="color:#d0d0d0">{</span>cmake,ament_cmake,ament_python<span style="color:#d0d0d0">}</span>
                        The build type to process the package with
  <span style="color:#f4bf75">--dependencies</span> DEPENDENCIES <span style="color:#d0d0d0">[</span>DEPENDENCIES ...]
                        list of dependencies
  <span style="color:#f4bf75">--maintainer-email</span> MAINTAINER_EMAIL
                        email address of the maintainer of this package
  <span style="color:#f4bf75">--maintainer-name</span> MAINTAINER_NAME
                        name of the maintainer of this package
  <span style="color:#f4bf75">--node-name</span> NODE_NAME
                        name of the empty executable
  <span style="color:#f4bf75">--library-name</span> LIBRARY_NAME
                        name of the empty library

</code></span></span></span></span>

我们先来看一下创建一个包需要的几个主要参数。

  • package_name是必须参数。想必大家都能理解。
  • --license是这个包支持的LICENSE,我们来查看一下:
    <span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg create <span style="color:#f4bf75">--license</span> ? my_test
    Supported licenses:
    Apache-2.0
    BSL-1.0
    BSD-2.0
    BSD-2-Clause
    BSD-3-Clause
    GPL-3.0-only
    LGPL-3.0-only
    MIT
    MIT-0
    </code></span></span></span>

    可以看出来它支持Apache-2.0BSL-1.0BSD-2.0BSD-2-ClauseBSD-3-ClauseGPL-3.0-onlyLGPL-3.0-onlyMITMIT-0这几种LICENSE。看一查看这里了解更多license.

  • --package-format {2,3}是这个包的package.xml格式,我们可以选择23。具体可以查看REP-0149.
  • --build-type是这个包的构建类型,我们可以选择cmake,ament_cmakeament_python来构建。
  • --dependencies是这个包的依赖关系,可以指定多个依赖。如果是多个依赖项,依次写在后面就行。后面也可以手动修改。
  • --maintainer-email--maintainer-name是这个包的维护者信息。
  • --node-name--library-name是这个包的可执行节点和库名称。
  • --destination-directory是这个包的生成目录,默认是当前目录。
  • --description是这个包的描述信息。

通过上面对于它的介绍,我们应该可以创建出一个新的包。我们来试试吧:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 入门教程提供的脚本,用来创建一个名字叫做my_package,节点名字叫做my_node的包。</span>
$ ros2 pkg create <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--node-name</span> my_node my_package
$ tree my_package/
my_package/
├── CMakeLists.txt
├── include
│   └── my_package
├── LICENSE
├── package.xml
└── src
    └── my_node.cpp

3 directories, 4 files
</code></span></span></span></span>

上面的文件结构和我们在上一小节的描述一致。我们现在再打开package.xml看一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75"><?xml version="1.0"?></span>
<span style="color:#f4bf75"><?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?></span>
<span style="color:#f4bf75"><package</span> <span style="color:#6a9fb5">format=</span><span style="color:#90a959">"3"</span><span style="color:#f4bf75">></span>
  <span style="color:#f4bf75"><name></span>my_package<span style="color:#f4bf75"></name></span>
  <span style="color:#f4bf75"><version></span>0.0.0<span style="color:#f4bf75"></version></span>
  <span style="color:#f4bf75"><description></span>TODO: Package description<span style="color:#f4bf75"></description></span>
  <span style="color:#f4bf75"><maintainer</span> <span style="color:#6a9fb5">email=</span><span style="color:#90a959">"zjh.2008.09@gmail.com"</span><span style="color:#f4bf75">></span>galileo<span style="color:#f4bf75"></maintainer></span>
  <span style="color:#f4bf75"><license></span>Apache-2.0<span style="color:#f4bf75"></license></span>

  <span style="color:#f4bf75"><buildtool_depend></span>ament_cmake<span style="color:#f4bf75"></buildtool_depend></span>

  <span style="color:#f4bf75"><test_depend></span>ament_lint_auto<span style="color:#f4bf75"></test_depend></span>
  <span style="color:#f4bf75"><test_depend></span>ament_lint_common<span style="color:#f4bf75"></test_depend></span>

  <span style="color:#f4bf75"><export></span>
    <span style="color:#f4bf75"><build_type></span>ament_cmake<span style="color:#f4bf75"></build_type></span>
  <span style="color:#f4bf75"></export></span>
<span style="color:#f4bf75"></package></span>
</code></span></span></span></span>

可以看到默认的package-format是3.0; 版本号默认是0.0.0; 因为我们没有制定依赖项所以也看不到相关信息(只有测试依赖项);默认的描述信息现实的是TODO: Package description。maintainer信息尽管我没有专门制定,但是也会默认使用我自己的名字。license是我们指定的Apache-2.0。build_type是ament_cmake。

现在我们依照上面的办法办法来创建一个支持ament python的package.

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg create <span style="color:#f4bf75">--build-type</span> ament_python <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--node-name</span> my_2nd_node my_2nd_package
$ tree my_2nd_package/
my_2nd_package/
├── LICENSE
├── my_2nd_package
│   ├── __init__.py
│   └── my_2nd_node.py
├── package.xml
├── resource
│   └── my_2nd_package
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

3 directories, 10 files
</code></span></span></span></span>

这样我就创建了一个支持名称叫做my_2nd_package的ament python的包。目录结构和之前提到的一致。我们打开package.xml看一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75"><?xml version="1.0"?></span>
<span style="color:#f4bf75"><?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?></span>
<span style="color:#f4bf75"><package</span> <span style="color:#6a9fb5">format=</span><span style="color:#90a959">"3"</span><span style="color:#f4bf75">></span>
  <span style="color:#f4bf75"><name></span>my_2nd_package<span style="color:#f4bf75"></name></span>
  <span style="color:#f4bf75"><version></span>0.0.0<span style="color:#f4bf75"></version></span>
  <span style="color:#f4bf75"><description></span>TODO: Package description<span style="color:#f4bf75"></description></span>
  <span style="color:#f4bf75"><maintainer</span> <span style="color:#6a9fb5">email=</span><span style="color:#90a959">"zjh.2008.09@gmail.com"</span><span style="color:#f4bf75">></span>galileo<span style="color:#f4bf75"></maintainer></span>
  <span style="color:#f4bf75"><license></span>Apache-2.0<span style="color:#f4bf75"></license></span>

  <span style="color:#f4bf75"><test_depend></span>ament_copyright<span style="color:#f4bf75"></test_depend></span>
  <span style="color:#f4bf75"><test_depend></span>ament_flake8<span style="color:#f4bf75"></test_depend></span>
  <span style="color:#f4bf75"><test_depend></span>ament_pep257<span style="color:#f4bf75"></test_depend></span>
  <span style="color:#f4bf75"><test_depend></span>python3-pytest<span style="color:#f4bf75"></test_depend></span>

  <span style="color:#f4bf75"><export></span>
    <span style="color:#f4bf75"><build_type></span>ament_python<span style="color:#f4bf75"></build_type></span>
  <span style="color:#f4bf75"></export></span>
<span style="color:#f4bf75"></package></span>
</code></span></span></span></span>

可以看到默认的package-format是3.0; 版本号默认是0.0.0; 因为我们没有指定依赖项所以也看不到相关信息(只有测试依赖项);默认的描述信息现实的是TODO: Package description。maintainer信息尽管我没有专门制定,但是也会默认使用我自己的名字。license是我们指定的Apache-2.0。build_type是ament_python。

python包中还专门提到了__init__.py这个文件,我们不妨也看一下它的内容。结果一查看,里面默认的内容为空。

现在我们来构建刚才创建的两个包。这一次我们根据入门教程的建议,先使用build直接构建。然后修改一部分内容,单独构建某一个包。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 构建整个工作区</span>
$ cd ../
$ colcon build
<span style="color:#d0d0d0">[</span>0.792s] WARNING:colcon.colcon_core.package_selection:Some selected packages are already built <span style="color:#aa759f">in </span>one or more underlay workspaces:
	<span style="color:#90a959">'turtlesim'</span> is <span style="color:#aa759f">in</span>: /home/galileo/Workspaces/ROS2/execises/demo2_ws/install/turtlesim, /opt/ros/humble
If a package <span style="color:#aa759f">in </span>a merged underlay workspace is overridden and it installs headers, <span style="color:#aa759f">then </span>all packages <span style="color:#aa759f">in </span>the overlay must sort their include directories by workspace order. Failure to <span style="color:#aa759f">do </span>so may result <span style="color:#aa759f">in </span>build failures or undefined behavior at run time.
If the overridden package is used by another package <span style="color:#aa759f">in </span>any underlay, <span style="color:#aa759f">then </span>the overriding package <span style="color:#aa759f">in </span>the overlay must be API and ABI compatible or undefined behavior at run time may occur.

If you understand the risks and want to override a package anyways, add the following to the command line:
	<span style="color:#f4bf75">--allow-overriding</span> turtlesim

This may be promoted to an error <span style="color:#aa759f">in </span>a future release of colcon-override-check.
Starting <span style="color:#d0d0d0">>>></span> my_2nd_package
Starting <span style="color:#d0d0d0">>>></span> my_package
Starting <span style="color:#d0d0d0">>>></span> turtlesim
<span style="color:#f4bf75">---</span> stderr: my_2nd_package                                                                                      
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> my_2nd_package <span style="color:#d0d0d0">[</span>0.94s]
Finished <span style="color:#d0d0d0"><<<</span> my_package <span style="color:#d0d0d0">[</span>1.15s]                                                           
Finished <span style="color:#d0d0d0"><<<</span> turtlesim <span style="color:#d0d0d0">[</span>8.34s]                     

Summary: 3 packages finished <span style="color:#d0d0d0">[</span>9.05s]
  1 package had stderr output: my_2nd_package
</code></span></span></span></span>

我们不妨来分别运行两个包,看一下结果。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/local_setup.bash
$ ros2 pkg list| grep my
dummy_map_server
dummy_robot_bringup
dummy_sensors
my_2nd_package
my_package
<span style="color:#888888">## 运行my_package</span>
$ ros2 run my_package my_node
hello world my_package package
<span style="color:#888888">## 运行my_2nd_package</span>
$ ros2 run my_2nd_package my_2nd_node
Hi from my_2nd_package.
</code></span></span></span></span>

现在我们修改一下的my_package输出内容。我们在my_node.cpp原来printf函数下面添加一行printf("Hallo! Wie geht's?\n");。然后重新编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 构建单个包</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> my_package
Starting <span style="color:#d0d0d0">>>></span> my_package
Finished <span style="color:#d0d0d0"><<<</span> my_package <span style="color:#d0d0d0">[</span>0.49s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.11s]
$ ros2 run my_package my_node
hello world my_package package
Hallo! Wie geht<span style="color:#90a959">'s?
</span></code></span></span></span></span>

这样我们就完成了编译。(因为我们已经source过一次overlay,这一次其实修改没有涉及到依赖项的添加,所以没有再重新source.)

现在我们再修改一下my_2nd_package的输出内容。我们在my_2nd_node.py原来print函数下面添加一行print(‘Wie ist das wetter?’)。因为是python,请注意换行的格式。然后重新编译和测试:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 构建单个包</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> my_2nd_package 
Starting <span style="color:#d0d0d0">>>></span> my_2nd_package
<span style="color:#f4bf75">---</span> stderr: my_2nd_package                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> my_2nd_package <span style="color:#d0d0d0">[</span>0.80s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.43s]
  1 package had stderr output: my_2nd_package

$ ros2 run my_2nd_package my_2nd_node
Hi from my_2nd_package.
Wie ist das wetter?
</code></span></span></span></span>
2.3.4 修改package.xml

刚才提到过package.xml文件可以被修改,我们现在讲描述信息修改掉吧。

唯一需要说明的是,ament_python的setup.py中也包含package.xml文件的信息,所以我们需要两处同步修改。

现在你可以使用$ ros2 pkg xml {package_name}去查看这个包的package.xml内容了。

2.4 尝试编写ament_cmake包

本小节参照入门教程Writing a simple publisher and subscriber (C++)的内容。

这一小节,包的创建和编译还是参照2.2和2.3小节的内容进行。主要是通过编程来深化node(节点)和topic(话题)的概念。其实程序功能比较简单,一个节点发布内容到一个Topic上,另一个订阅内容这个Topic。

这一次我们新建一个工作区,名字叫做demo3_ws. 然后创建一个名字叫做cpp_pubsub的package。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ mkdir <span style="color:#f4bf75">-p</span> demo3_ws/src
$ cd demo3_ws/src
$ ros2 pkg create <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--destination-directory</span> ./src <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"A simple publisher and subscriber node in C++"</span> cpp_pubsub
$ ls src/cpp_pubsub/
CMakeLists.txt  include  LICENSE  package.xml  src
</code></span></span></span></span>

至此,我们已经创建了一个名字叫做cpp_pubsub的包。我们这次将从这里下载一个源文件放到src目录下。可以使用wget或者直接从浏览器现在,然后手动放入也可以。我选择前者:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ wget <span style="color:#f4bf75">-O</span> src/cpp_pubsub/src/publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp
<span style="color:#888888">## 省略回应内容 ...</span>
<span style="color:#888888">## 下载完成之后,检查一下</span>
$ ls src/cpp_pubsub/src/
publisher_member_function.cpp
<span style="color:#888888">## 然后使用你最喜欢的工具查看和修改文档,我使用vscode</span>
$ code src/cpp_pubsub/
</code></span></span></span></span>
2.4.1 学习这段示例代码

开头有几个c++11引入的头文件:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">/*This header is part of the date and time library.*/</span>
<span style="color:#f4bf75">#include <chrono>
</span><span style="color:#888888">/*This header is part of the function objects library and provides the standard hash function.*/</span>
<span style="color:#f4bf75">#include <functional>
</span><span style="color:#888888">/*This header is part of the dynamic memory management library.*/</span>
<span style="color:#f4bf75">#include <memory>
#include <string>
</span></code></span></span></span></span>

关于chrono的描述可以看这里chrono;关于functional的描述可以看这里functional;关于memory的描述可以看这里memory。

接下来有几个与ros相关的library中的头文件:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">/*`rclcpp` provides the canonical C++ API for interacting with ROS.*/</span>
<span style="color:#888888">/*It consists of these main components:Node,Publisher,Subscriber,Servic Client,Servic Server,Timer,Parameter,Rate, Executors, CallbackGroups ... and many more*/</span>
<span style="color:#f4bf75">#include "rclcpp/rclcpp.hpp"
</span>
<span style="color:#f4bf75">#include "std_msgs/msg/string.hpp"
</span></code></span></span></span></span>

如果有必要,可以手动打开查看。请记住这一步我们使用了rclcpp和std_msgs这两个ROS的包。所以后面需要在依赖项中有所体现。

再来看一下主函数:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span> <span style="color:#d0d0d0">*</span> argv<span style="color:#d0d0d0">[])</span>
<span style="color:#d0d0d0">{</span>
  rclcpp<span style="color:#d0d0d0">::</span>init<span style="color:#d0d0d0">(</span>argc<span style="color:#d0d0d0">,</span> argv<span style="color:#d0d0d0">);</span>
  rclcpp<span style="color:#d0d0d0">::</span>spin<span style="color:#d0d0d0">(</span>std<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0"><</span>MinimalPublisher<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">());</span>
  rclcpp<span style="color:#d0d0d0">::</span>shutdown<span style="color:#d0d0d0">();</span>
  <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

可以大体理解先初始化,然后循环执行MinimalPublisher这个类的构造函数,最后关闭。所以再来看一下MinimalPublisher这个类。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalPublisher</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> rclcpp<span style="color:#d0d0d0">::</span>Node
<span style="color:#d0d0d0">{</span>
public:
  MinimalPublisher<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">:</span> Node<span style="color:#d0d0d0">(</span><span style="color:#90a959">"minimal_publisher"</span><span style="color:#d0d0d0">),</span> count_<span style="color:#d0d0d0">(</span><span style="color:#90a959">0</span><span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    publisher_ <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_publisher<span style="color:#d0d0d0"><</span>std_msgs<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>String<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">(</span><span style="color:#90a959">"topic"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">10</span><span style="color:#d0d0d0">);</span>
    timer_ <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_wall_timer<span style="color:#d0d0d0">(</span>
      <span style="color:#90a959">500ms</span><span style="color:#d0d0d0">,</span> std<span style="color:#d0d0d0">::</span>bind<span style="color:#d0d0d0">(</span><span style="color:#d0d0d0">&</span>MinimalPublisher<span style="color:#d0d0d0">::</span>timer_callback<span style="color:#d0d0d0">,</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">));</span>
  <span style="color:#d0d0d0">}</span>

private:
  <span style="color:#d28445">void</span> timer_callback<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">{</span>
    <span style="color:#aa759f">auto</span> message <span style="color:#d0d0d0">=</span> std_msgs<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>String<span style="color:#d0d0d0">();</span>
    message<span style="color:#d0d0d0">.</span>data <span style="color:#d0d0d0">=</span> <span style="color:#90a959">"Hello, world! "</span> <span style="color:#d0d0d0">+</span> std<span style="color:#d0d0d0">::</span>to_string<span style="color:#d0d0d0">(</span>count_<span style="color:#d0d0d0">++</span><span style="color:#d0d0d0">);</span>
    RCLCPP_INFO<span style="color:#d0d0d0">(</span><span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>get_logger<span style="color:#d0d0d0">(),</span> <span style="color:#90a959">"Publishing: '%s'"</span><span style="color:#d0d0d0">,</span> message<span style="color:#d0d0d0">.</span>data<span style="color:#d0d0d0">.</span>c_str<span style="color:#d0d0d0">());</span>
    publisher_<span style="color:#d0d0d0">-></span>publish<span style="color:#d0d0d0">(</span>message<span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span>
  rclcpp<span style="color:#d0d0d0">::</span>TimerBase<span style="color:#d0d0d0">::</span>SharedPtr timer_<span style="color:#d0d0d0">;</span>
  rclcpp<span style="color:#d0d0d0">::</span>Publisher<span style="color:#d0d0d0"><</span>std_msgs<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>String<span style="color:#d0d0d0">>::</span>SharedPtr publisher_<span style="color:#d0d0d0">;</span>
  <span style="color:#d28445">size_t</span> count_<span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">};</span>
</code></span></span></span></span>

这个函数就比较复杂了。看起来我们还是需要先来理解一些基本知识。

2.4.2 rclcpp和std_msgs学习
  • static_assert:
  • std::bind:
  • RCLCPP_INFO:
  • rclcpp::init:
  • rclcpp::spin
  • rclcpp::shutdown
  • rclcpp::Node
  • rclcpp::create_publisher
  • rclcpp::create_wall_timer

这部分函数的功能比较简单,但是用到的函数还是比较复杂的。

2.4.3 配置依赖项

前面提到了我们用到了rclcpp和std_msgs这两个包,所以我们需要在package.xml中添加依赖项。添加内容:

<depend>rclcpp</depend>
<depend>std_msgs</depend>

CMakelists.txt文件内容也要修改,在适当位置添加:

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

此外还需要添加一个默认的C++版本:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888"># Default to C++14</span>
<span style="color:#aa759f">if</span><span style="color:#d0d0d0">(</span>NOT CMAKE_CXX_STANDARD<span style="color:#d0d0d0">)</span>
  set<span style="color:#d0d0d0">(</span>CMAKE_CXX_STANDARD 14<span style="color:#d0d0d0">)</span>
endif<span style="color:#d0d0d0">()</span>
</code></span></span></span></span>

做完这些内容,我们构建一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 检查一下依赖</span>
$ rosdep check <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--ignore-src</span> <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
All system dependencies have been satisfied

<span style="color:#888888">## 我们也可以用另一条指令</span>
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--ignore-src</span> <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build
Starting <span style="color:#d0d0d0">>>></span> cpp_pubsub
Finished <span style="color:#d0d0d0"><<<</span> cpp_pubsub <span style="color:#d0d0d0">[</span>3.95s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>4.55s]
</code></span></span></span></span>
2.4.3 在这个package中间添加一个subscriber节点

照例从这里下载文件。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ wget <span style="color:#f4bf75">-O</span> src/cpp_pubsub/src/subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp
$ ls src/cpp_pubsub/src/
publisher_member_function.cpp  subscriber_member_function.cpp
</code></span></span></span></span>

同样的办法我们查看一下代码。具体这里就不再描述。因为这个源文件同样使用rclcpp和std_msgs这两个包。所以不必要再修改package.xml。但是需要向CMakeLists.txt中添加一个新的target。 新增:

add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)

修改:

install(TARGETS
  talker 
  listener
  DESTINATION lib/${PROJECT_NAME})

OK,现在我们再来构建一次:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>做完这些内容,我们构建一下:
<span style="color:#90a959">```</span>bash
<span style="color:#888888">## 检查一下依赖</span>
$ rosdep check <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--ignore-src</span> <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
All system dependencies have been satisfied

<span style="color:#888888">## 我们也可以用另一条指令</span>
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--ignore-src</span> <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build
Starting <span style="color:#d0d0d0">>>></span> cpp_pubsub
Finished <span style="color:#d0d0d0"><<<</span> cpp_pubsub <span style="color:#d0d0d0">[</span>4.87s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>5.53s]
</code></span></span></span></span>

这一次可以不用检查依赖项。不过多做一步问题不大。

现在再来source一下overlay,然后测试:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 pkg list | grep cpp_pubsub
cpp_pubsub
<span style="color:#888888">## 检查一下可执行程序</span>
$ ros2 pkg executables cpp_pubsub
cpp_pubsub listener
cpp_pubsub talker
<span style="color:#888888">## 运行listener</span>
$ ros2 run cpp_pubsub listener
</code></span></span></span></span>

同样的方法在另一个终端运行talker:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 run cpp_pubsub talker
</code></span></span></span></span>

可以看到listener订阅到了talker发布的消息。

cpp_pubsub测试图

图6:cpp_pubsub的测试图

需要说明的使用ros2 pkg executables cpp_pubsub检查到的listenertalker其实是在CMakelists.txt文件中定义的。

2.4.4 总结

这一章节使用了入门教程提供的示例代码来测试两个node之间通过topic进行通讯。代码尽管不复杂,但是有很多地方需要详细了解才行。另外代码使用了modern c++。看起来后面还要更新自己的modern c++知识。

在这一节我们了解到了rclcpp中的Node::create_publisher和Node::create_subscription这两个函数。这两个函数可以用来创建发布者和订阅者。此外我们还学会使用rclcpp中的Node::create_wall_timer函数来创建定时器。

2.5 尝试编写ament_python包

本小节参照入门教程Writing a simple publisher and subscriber (Python)的内容。这一部分的功能和2.4节的基本一致,只是代码是用python编写的。

这一节我们新建一个工作区名称叫做demo4_ws,然后在里面创建一个名称是py_pubsub类型是ament_python的package。如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ mkdir -p demo4_ws/src
$ cd demo4_ws
$ ros2 pkg create py_pubsub --build-type ament_python --license "Apache-2.0" --destination-directory src --description "A simple publisher and subscriber example in Python" 
$ tree src/py_pubsub/
src/py_pubsub/
├── LICENSE
├── package.xml
├── py_pubsub
│   └── __init__.py
├── resource
│   └── py_pubsub
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

3 directories, 9 files
</code></span></span></span></span>

照例我们需要从示例库分别下载两份文件,一份是publisher_member_function.py;另一份是subscriber_member_function.py。这两个文件正如名称所表示的那样一个是publisher的代码一个是subscriber的代码。下载完成之后,我们可以使用vscode打开工程检视代码。 如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ wget <span style="color:#f4bf75">-O</span> src/py_pubsub/py_pubsub/publisher_member_function.py https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py
$ wget <span style="color:#f4bf75">-O</span> src/py_pubsub/py_pubsub/subscriber_member_function.py https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py
$ ls src/py_pubsub/py_pubsub/
__init__.py  publisher_member_function.py  subscriber_member_function.py
$ code src/py_pubsub/
</code></span></span></span></span>

我们先从publisher_member_function.py这个文件开始 开头三行

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">import</span> <span style="color:#f4bf75">rclpy</span>
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">rclpy.node</span> <span style="color:#aa759f">import</span> Node

<span style="color:#aa759f">from</span> <span style="color:#f4bf75">std_msgs.msg</span> <span style="color:#aa759f">import</span> String
</code></span></span></span></span>

这两行导入了ROS2的一些基础包,其中rclpy是ROS2的python接口,Node是ROS2的节点基类。std_msgs是ROS2的标准消息类型。整个程序从main开始:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">def</span> main<span style="color:#d0d0d0">(</span>args<span style="color:#d0d0d0">=</span>None<span style="color:#d0d0d0">):</span>
    rclpy<span style="color:#d0d0d0">.</span>init<span style="color:#d0d0d0">(</span>args<span style="color:#d0d0d0">=</span>args<span style="color:#d0d0d0">)</span>

    minimal_publisher <span style="color:#d0d0d0">=</span> MinimalPublisher<span style="color:#d0d0d0">()</span>

    rclpy<span style="color:#d0d0d0">.</span>spin<span style="color:#d0d0d0">(</span>minimal_publisher<span style="color:#d0d0d0">)</span>

    <span style="color:#888888"># Destroy the node explicitly
</span>    <span style="color:#888888"># (optional - otherwise it will be done automatically
</span>    <span style="color:#888888"># when the garbage collector destroys the node object)
</span>    minimal_publisher<span style="color:#d0d0d0">.</span>destroy_node<span style="color:#d0d0d0">()</span>
    rclpy<span style="color:#d0d0d0">.</span>shutdown<span style="color:#d0d0d0">()</span>


<span style="color:#aa759f">if</span> __name__ <span style="color:#d0d0d0">==</span> <span style="color:#90a959">'__main__'</span><span style="color:#d0d0d0">:</span>
    main<span style="color:#d0d0d0">()</span>
</code></span></span></span></span>

main函数的逻辑基本和ament_cmake的一致。先调用rclpy.init初始化ROS2环境,然后创建了一个MinimalPublisher的实例,最后调用rclpy.spin让节点持续运行,直到节点被销毁(rclpy.shutdown)。在shutdown之前,这个函数还调用了 minimal_publisher.destroy_node()去销毁节点。这一点在前面的程序中没有看到。

MinimalPublisher类如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalPublisher</span><span style="color:#d0d0d0">(</span>Node<span style="color:#d0d0d0">):</span>

    <span style="color:#aa759f">def</span> __init__<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        super<span style="color:#d0d0d0">().</span>__init__<span style="color:#d0d0d0">(</span><span style="color:#90a959">'minimal_publisher'</span><span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>publisher_ <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>create_publisher<span style="color:#d0d0d0">(</span>String<span style="color:#d0d0d0">,</span> <span style="color:#90a959">'topic'</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">10</span><span style="color:#d0d0d0">)</span>
        timer_period <span style="color:#d0d0d0">=</span> <span style="color:#90a959">0.5</span>  <span style="color:#888888"># seconds
</span>        self<span style="color:#d0d0d0">.</span>timer <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>create_timer<span style="color:#d0d0d0">(</span>timer_period<span style="color:#d0d0d0">,</span> self<span style="color:#d0d0d0">.</span>timer_callback<span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>i <span style="color:#d0d0d0">=</span> <span style="color:#90a959">0</span>

    <span style="color:#aa759f">def</span> timer_callback<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        msg <span style="color:#d0d0d0">=</span> String<span style="color:#d0d0d0">()</span>
        msg<span style="color:#d0d0d0">.</span>data <span style="color:#d0d0d0">=</span> <span style="color:#90a959">'Hello World: %d'</span> <span style="color:#d0d0d0">%</span> self<span style="color:#d0d0d0">.</span>i
        self<span style="color:#d0d0d0">.</span>publisher_<span style="color:#d0d0d0">.</span>publish<span style="color:#d0d0d0">(</span>msg<span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>get_logger<span style="color:#d0d0d0">().</span>info<span style="color:#d0d0d0">(</span><span style="color:#90a959">'Publishing: "%s"'</span> <span style="color:#d0d0d0">%</span> msg<span style="color:#d0d0d0">.</span>data<span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>i <span style="color:#d0d0d0">+=</span> <span style="color:#90a959">1</span>
</code></span></span></span></span>

这一部分逻辑也比较简单使用。整体创建了MinimalPublisher的类,继承自rclpy.node。然后使用rclpy.node.create_timer创建一个500ms调用一次的定时器。在timer_callback函数中,创建了一个std_msgs.msg.String类型的消息,设置了要发布的信息,发布并打印日志。 这中间引用了很多rclpy和std_msgs的API。后面慢慢学习吧,不可能一蹴而就。

然后我们再来看看subscriber_member_function.py的代码。开头也是引用了rclpy和std_msgs的包。main函数的流程也一致,只是这次创建了一个名称叫做MinimalSubscriber的类,继承自rclpy.node。然后使用rclpy.node.create_subscription订阅了topic。订阅的回调函数是listener_callback。在这个函数里面使用rclpy.node.get_logger打印日志。

现在再来修改一下package.xml文件,添加依赖项。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75"><exec_depend></span>rclpy<span style="color:#f4bf75"></exec_depend></span>
<span style="color:#f4bf75"><exec_depend></span>std_msgs<span style="color:#f4bf75"></exec_depend></span>
</code></span></span></span></span>

注意这个和ament_cmake的标签不一样。后者之前使用的是一个depend标签。 还记得setup.cfg和setup.py这两个文件的区别吗:

  • setup.cfg 当包有可执行文件时需要setup.cfg,因此ros2 run可以找到它们
  • setup.py 包含如何安装包的说明

现在需要修改setup.py,来添加执行点(entry point):

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>entry_points<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">{</span>
        <span style="color:#90a959">'console_scripts'</span><span style="color:#d0d0d0">:</span> <span style="color:#d0d0d0">[</span>
                <span style="color:#90a959">'talker = py_pubsub.publisher_member_function:main'</span><span style="color:#d0d0d0">,</span>
        <span style="color:#d0d0d0">],</span>
<span style="color:#d0d0d0">},</span>
</code></span></span></span></span>

现在再来检查一下setup.cfg文件。内容应当是:

[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub

现在开始检查依赖,然后build整个package.

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build
<span style="color:#888888">## 如果你里面有多个package,也可以</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> py_pubsub
Starting <span style="color:#d0d0d0">>>></span> py_pubsub
<span style="color:#f4bf75">---</span> stderr: py_pubsub                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> py_pubsub <span style="color:#d0d0d0">[</span>0.89s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.51s]
  1 package had stderr output: py_pubsub
</code></span></span></span></span>

接着source一下overlay.并在两个终端分别运行talker和listener。 在一个终端执行:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 pkg list | grep py_pubsub
py_pubsub
$ ros2 pkg executables py_pubsub
py_pubsub listener
py_pubsub talker
$ ros2 run py_pubsub listener
</code></span></span></span></span>

在另一个终端执行: 在一个终端

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 pkg list | grep py_pubsub
py_pubsub
$ ros2 run py_pubsub talker
</code></span></span></span></span>

测试效果如下: 

py_pubsub_test

图7:py_pubsub的测试图

请注意这里的talkerlistener的名称其实是在前面setup.py文件中定义的。

2.5.2 总结

这一章节使用了入门教程提供的示例代码来测试两个node之间通过topic进行通讯。代码尽管不复杂,但是有很多地方需要详细了解才行。

在这一节我们了解到了rclpy中的Node::create_publisher和Node::create_subscription这两个函数。这两个函数可以用来创建发布者和订阅者。此外我们还学会使用rclpy中的Node::create_timer函数来创建定时器。

2.6 使用c++编写ROS2的server和client

本小节参照入门教程Writing a simple service and client (C++)的内容。这一部分主要演示了ROS2的service怎么使用。在这一章你会发现请求和响应的结构由.srv文件决定。

这一次我们建立一个新的工作空间叫做demo5_ws:。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 你需要先导航到你放置练习工程的目录中</span>
$ mkdir <span style="color:#f4bf75">-p</span> demo5_ws/src
<span style="color:#888888">## 和前面一样我在操作时始终位于工作区根目录,这一点和官方热门不同,因此命令有一些区别</span>
$ cd cmke_ws
$ 
</code></span></span></span></span>

然后我们来创建一个package,名字叫做cpp_srvcli,依赖于rclcpp和example_interfaces,构建类型还是ament_cmake,license还是“Apache-2.0”.请注意命令中名称的位置,要防止写在--dependencies后面。另外请注意example_interfaces也是一个package,它包含构建请求和响应所需的.srv文件的包。你可以通过’ros2 pkg list’看到它。至于.srv的格式后面在专门做出说明。

如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg list | grep example_
example_interfaces
$ ros2 pkg create cpp_srvcli  <span style="color:#f4bf75">--destination-directory</span> src  <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--dependencies</span> rc
</code></span></span></span></span>
2.6.1 创建server程序

本次将创建一个求和(sum)服务。我们照例打开vscode编辑代码。这次我们将代码写在一个名称叫做add_two_ints_server.cpp的文件中。文件中代码如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
</span>
<span style="color:#f4bf75">#include <memory>
</span>
<span style="color:#d28445">void</span> add<span style="color:#d0d0d0">(</span><span style="color:#aa759f">const</span> std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">::</span>Request<span style="color:#d0d0d0">></span> request<span style="color:#d0d0d0">,</span>
          std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">::</span>Response<span style="color:#d0d0d0">></span>      response<span style="color:#d0d0d0">)</span>
<span style="color:#d0d0d0">{</span>
  response<span style="color:#d0d0d0">-></span>sum <span style="color:#d0d0d0">=</span> request<span style="color:#d0d0d0">-></span>a <span style="color:#d0d0d0">+</span> request<span style="color:#d0d0d0">-></span>b<span style="color:#d0d0d0">;</span>
  RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"Incoming request</span><span style="color:#8f5536">\n</span><span style="color:#90a959">a: %ld"</span> <span style="color:#90a959">" b: %ld"</span><span style="color:#d0d0d0">,</span>
                request<span style="color:#d0d0d0">-></span>a<span style="color:#d0d0d0">,</span> request<span style="color:#d0d0d0">-></span>b<span style="color:#d0d0d0">);</span>
  RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"sending back response: [%ld]"</span><span style="color:#d0d0d0">,</span> <span style="color:#d0d0d0">(</span><span style="color:#d28445">long</span> <span style="color:#d28445">int</span><span style="color:#d0d0d0">)</span>response<span style="color:#d0d0d0">-></span>sum<span style="color:#d0d0d0">);</span>
<span style="color:#d0d0d0">}</span>

<span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span> <span style="color:#d0d0d0">**</span>argv<span style="color:#d0d0d0">)</span>
<span style="color:#d0d0d0">{</span>
  rclcpp<span style="color:#d0d0d0">::</span>init<span style="color:#d0d0d0">(</span>argc<span style="color:#d0d0d0">,</span> argv<span style="color:#d0d0d0">);</span>

  std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">></span> node <span style="color:#d0d0d0">=</span> rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints_server"</span><span style="color:#d0d0d0">);</span>

  rclcpp<span style="color:#d0d0d0">::</span>Service<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">>::</span>SharedPtr service <span style="color:#d0d0d0">=</span>
    node<span style="color:#d0d0d0">-></span>create_service<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints"</span><span style="color:#d0d0d0">,</span> <span style="color:#d0d0d0">&</span>add<span style="color:#d0d0d0">);</span>

  RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"Ready to add two ints."</span><span style="color:#d0d0d0">);</span>

  rclcpp<span style="color:#d0d0d0">::</span>spin<span style="color:#d0d0d0">(</span>node<span style="color:#d0d0d0">);</span>
  rclcpp<span style="color:#d0d0d0">::</span>shutdown<span style="color:#d0d0d0">();</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

这段代码看似简单,但是本质还是挺复杂的。如果你深入example_interfaces::srv::AddTwoInts::Request和example_interfaces::srv::AddTwoInts::Response去查看,会发现AddTwoInts是一个很复杂的类型。里面用到了很多modern c的新特性。

这段代码的功能其实就是从request里面获取a和b两个变量的值然后相加,再将结果返回给response,同时在服务器这一侧使用rclcpp::get_logger打印必要的logger.

程序创建节点使用了rclcpp::Node::make_shared函数实现的:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">></span> node <span style="color:#d0d0d0">=</span> rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints_server"</span><span style="color:#d0d0d0">);</span>
</code></span></span></span></span>

为该节点创建一个名为 add_two_ints 的服务,并使用 &add 方法自动在网络上通告它:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>rclcpp<span style="color:#d0d0d0">::</span>Service<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">>::</span>SharedPtr service <span style="color:#d0d0d0">=</span>
node<span style="color:#d0d0d0">-></span>create_service<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints"</span><span style="color:#d0d0d0">,</span> <span style="color:#d0d0d0">&</span>add<span style="color:#d0d0d0">);</span>
</code></span></span></span></span>
2.6.2 创建client程序

这部分我们将代码写在一个名称叫做add_two_ints_client.cpp的文件中。文件中代码如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
</span>
<span style="color:#f4bf75">#include <chrono>
#include <cstdlib>
#include <memory>
</span>
<span style="color:#aa759f">using</span> <span style="color:#aa759f">namespace</span> std<span style="color:#d0d0d0">::</span>chrono_literals<span style="color:#d0d0d0">;</span>

<span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span> <span style="color:#d0d0d0">**</span>argv<span style="color:#d0d0d0">)</span>
<span style="color:#d0d0d0">{</span>
  rclcpp<span style="color:#d0d0d0">::</span>init<span style="color:#d0d0d0">(</span>argc<span style="color:#d0d0d0">,</span> argv<span style="color:#d0d0d0">);</span>

  <span style="color:#aa759f">if</span> <span style="color:#d0d0d0">(</span>argc <span style="color:#d0d0d0">!=</span> <span style="color:#90a959">3</span><span style="color:#d0d0d0">)</span> <span style="color:#d0d0d0">{</span>
      RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"usage: add_two_ints_client X Y"</span><span style="color:#d0d0d0">);</span>
      <span style="color:#aa759f">return</span> <span style="color:#90a959">1</span><span style="color:#d0d0d0">;</span>
  <span style="color:#d0d0d0">}</span>

  std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">></span> node <span style="color:#d0d0d0">=</span> rclcpp<span style="color:#d0d0d0">::</span>Node<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints_client"</span><span style="color:#d0d0d0">);</span>
  rclcpp<span style="color:#d0d0d0">::</span>Client<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">>::</span>SharedPtr client <span style="color:#d0d0d0">=</span>
    node<span style="color:#d0d0d0">-></span>create_client<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">(</span><span style="color:#90a959">"add_two_ints"</span><span style="color:#d0d0d0">);</span>

  <span style="color:#aa759f">auto</span> request <span style="color:#d0d0d0">=</span> std<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0"><</span>example_interfaces<span style="color:#d0d0d0">::</span>srv<span style="color:#d0d0d0">::</span>AddTwoInts<span style="color:#d0d0d0">::</span>Request<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">();</span>
  request<span style="color:#d0d0d0">-></span>a <span style="color:#d0d0d0">=</span> atoll<span style="color:#d0d0d0">(</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">1</span><span style="color:#d0d0d0">]);</span>
  request<span style="color:#d0d0d0">-></span>b <span style="color:#d0d0d0">=</span> atoll<span style="color:#d0d0d0">(</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">2</span><span style="color:#d0d0d0">]);</span>

  <span style="color:#aa759f">while</span> <span style="color:#d0d0d0">(</span><span style="color:#d0d0d0">!</span>client<span style="color:#d0d0d0">-></span>wait_for_service<span style="color:#d0d0d0">(</span><span style="color:#90a959">1s</span><span style="color:#d0d0d0">))</span> <span style="color:#d0d0d0">{</span>
    <span style="color:#aa759f">if</span> <span style="color:#d0d0d0">(</span><span style="color:#d0d0d0">!</span>rclcpp<span style="color:#d0d0d0">::</span>ok<span style="color:#d0d0d0">())</span> <span style="color:#d0d0d0">{</span>
      RCLCPP_ERROR<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"Interrupted while waiting for the service. Exiting."</span><span style="color:#d0d0d0">);</span>
      <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
    <span style="color:#d0d0d0">}</span>
    RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"service not available, waiting again..."</span><span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span>

  <span style="color:#aa759f">auto</span> result <span style="color:#d0d0d0">=</span> client<span style="color:#d0d0d0">-></span>async_send_request<span style="color:#d0d0d0">(</span>request<span style="color:#d0d0d0">);</span>
  <span style="color:#888888">// Wait for the result.</span>
  <span style="color:#aa759f">if</span> <span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>spin_until_future_complete<span style="color:#d0d0d0">(</span>node<span style="color:#d0d0d0">,</span> result<span style="color:#d0d0d0">)</span> <span style="color:#d0d0d0">==</span>
    rclcpp<span style="color:#d0d0d0">::</span>FutureReturnCode<span style="color:#d0d0d0">::</span>SUCCESS<span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    RCLCPP_INFO<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"Sum: %ld"</span><span style="color:#d0d0d0">,</span> result<span style="color:#d0d0d0">.</span>get<span style="color:#d0d0d0">()</span><span style="color:#d0d0d0">-></span>sum<span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span> <span style="color:#aa759f">else</span> <span style="color:#d0d0d0">{</span>
    RCLCPP_ERROR<span style="color:#d0d0d0">(</span>rclcpp<span style="color:#d0d0d0">::</span>get_logger<span style="color:#d0d0d0">(</span><span style="color:#90a959">"rclcpp"</span><span style="color:#d0d0d0">),</span> <span style="color:#90a959">"Failed to call service add_two_ints"</span><span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span>

  rclcpp<span style="color:#d0d0d0">::</span>shutdown<span style="color:#d0d0d0">();</span>
  <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

这段代码也使用了智能指针,我会转么写一篇文章将智能指针。

这段代码先使用rclcpp.init初始化.然后创建node,使用rclcpp::Node::create_client创建client。接着创建request,并设置a和b的值。然后以1s的周期去检查服务器状态,如果服务器不可用就继续等待。rclcpp出错则会报错并退出。如果服务器可用就发送request并用异步方式等待回应,这段代码使用的是async_send_request来实现的.使用spin_until_future_complete去等待服务器的响应。如果完成就使用rclcpp::get_logger打印结果。最后关闭并推出。

这段程序中还是用到了atoll,它的作用和atol类似。atol是把字符串转成长整形(long int),atoll是把字符串转成长长整形(long long int)。主要原因是我们的srv文件中定义的服务输入是int64的:

int64 a
int64 b
---
int64 sum
2.6.3 元文件和编译规则设置

我们需要的两个依赖项是rclcpp和example_interfaces。我们需要在package.xml中添加它们.好在我们在创建的时候已经添加。现在只需要检查一下。现在向CMakeLists.txt文件添加依赖项:

add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)

add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client rclcpp example_interfaces)

install(TARGETS 
        server 
        client 
        DESTINATION lib/${PROJECT_NAME})

编辑好之后,我们检查依赖项并编译。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> cpp_srvcli
Starting <span style="color:#d0d0d0">>>></span> cpp_srvcli
Finished <span style="color:#d0d0d0"><<<</span> cpp_srvcli <span style="color:#d0d0d0">[</span>4.09s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>4.70s]
</code></span></span></span></span>
2.6.4 运行程序

在一个终端运行client:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
<span style="color:#888888">## 确保package可见</span>
$ ros2 pkg list | grep cpp_srvcli
cpp_srvcli
<span style="color:#888888">## 查询可执行程序</span>
$ ros2 pkg executables cpp_srvcli
cpp_srvcli client
cpp_srvcli service
<span style="color:#888888">## 运行client</span>
$ ros2 run cpp_srvcli client 12 56
</code></span></span></span></span>

请注意,理论上服务器要先运行。这里让client运行是为了验证client程序的等待过程是否会报错。我们可以故意等几秒钟再启动server:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
<span style="color:#888888">## 运行service</span>
$ ros2 run cpp_srvcli service
</code></span></span></span></span>

结果如下图8所示。 

cpp_srvcli测试图

图8:cpp_srvcli测试图

2.6.5 总结

本章我们学习了ROS2的service的使用,并编写了server和client程序。可以看出这部分的程序还是非常复杂的。至于srv我们本次使用了外部的srv,后面我们还需要学习自己编写srv文件和服务器和客户端的类的编写。

在这一节我们了解到了rclcpp中的Node::create_service创建服务器和使用Node::create_client创建客户端;也学会使用wait_for_service去等待服务器的可用性;也学会使用async_send_request函数去异步发送请求。

2.7 使用python编写ROS2的server和client

本小节参照入门教程Writing a simple service and client (Python)的内容。这一部分功能和上一小节基本一致,只是语言变成了python.

这一次我们建立一个新的工作空间叫做demo6_ws,之后接下来几篇涉及ament_python的工程都放在这个目录中。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 你需要先导航到你放置练工程的目录中</span>
$ mkdir <span style="color:#f4bf75">-p</span> demo6_ws/src
<span style="color:#888888">## 和前面一样我在操作时始终位于工作区根目录,因此命令有一些区别</span>
$ cd demo6_ws
</code></span></span></span></span>

然后我们来创建一个package,名字叫做py_srvcli,依赖于rclpy和example_interfaces,构建类型还是ament_python,license还是“Apache-2.0”.请注意命令中名称的位置,要防止写在--dependencies后面。另外请注意example_interfaces也是一个package,它包含构建请求和响应所需的.srv文件的包。你可以通过’ros2 pkg list’看到它。至于.srv的格式后面在专门做出说明。 如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg list | grep example_
example_interfaces
$ ros2 pkg create py_srvcli  <span style="color:#f4bf75">--destination-directory</span> src  <span style="color:#f4bf75">--build-type</span> ament_python <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--dependencies</span> rclpy example_interfaces <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"Python client server tutorial"</span>
$ tree src/py_srvcli/
src/py_srvcli/
├── LICENSE
├── package.xml
├── py_srvcli
│   └── __init__.py
├── resource
│   └── py_srvcli
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

3 directories, 9 files
</code></span></span></span></span>
2.7.1 创建server程序

本次将创建一个求和(sum)服务。我们照例打开vscode编辑代码。这次我们将代码写在一个名称叫做service_member_function.py的文件中(目录是src/py_srvcli/py_srvcli)。文件中代码如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">from</span> <span style="color:#f4bf75">example_interfaces.srv</span> <span style="color:#aa759f">import</span> AddTwoInts

<span style="color:#aa759f">import</span> <span style="color:#f4bf75">rclpy</span>
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">rclpy.node</span> <span style="color:#aa759f">import</span> Node


<span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalService</span><span style="color:#d0d0d0">(</span>Node<span style="color:#d0d0d0">):</span>

    <span style="color:#aa759f">def</span> __init__<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        super<span style="color:#d0d0d0">().</span>__init__<span style="color:#d0d0d0">(</span><span style="color:#90a959">'minimal_service'</span><span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>srv <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>create_service<span style="color:#d0d0d0">(</span>AddTwoInts<span style="color:#d0d0d0">,</span> <span style="color:#90a959">'add_two_ints'</span><span style="color:#d0d0d0">,</span> self<span style="color:#d0d0d0">.</span>add_two_ints_callback<span style="color:#d0d0d0">)</span>

    <span style="color:#aa759f">def</span> add_two_ints_callback<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">,</span> request<span style="color:#d0d0d0">,</span> response<span style="color:#d0d0d0">):</span>
        response<span style="color:#d0d0d0">.</span>sum <span style="color:#d0d0d0">=</span> request<span style="color:#d0d0d0">.</span>a <span style="color:#d0d0d0">+</span> request<span style="color:#d0d0d0">.</span>b
        self<span style="color:#d0d0d0">.</span>get_logger<span style="color:#d0d0d0">().</span>info<span style="color:#d0d0d0">(</span><span style="color:#90a959">'Incoming request</span><span style="color:#8f5536">\n</span><span style="color:#90a959">a: %d b: %d'</span> <span style="color:#d0d0d0">%</span> <span style="color:#d0d0d0">(</span>request<span style="color:#d0d0d0">.</span>a<span style="color:#d0d0d0">,</span> request<span style="color:#d0d0d0">.</span>b<span style="color:#d0d0d0">))</span>

        <span style="color:#aa759f">return</span> response


<span style="color:#aa759f">def</span> main<span style="color:#d0d0d0">():</span>
    rclpy<span style="color:#d0d0d0">.</span>init<span style="color:#d0d0d0">()</span>

    minimal_service <span style="color:#d0d0d0">=</span> MinimalService<span style="color:#d0d0d0">()</span>

    rclpy<span style="color:#d0d0d0">.</span>spin<span style="color:#d0d0d0">(</span>minimal_service<span style="color:#d0d0d0">)</span>

    rclpy<span style="color:#d0d0d0">.</span>shutdown<span style="color:#d0d0d0">()</span>


<span style="color:#aa759f">if</span> __name__ <span style="color:#d0d0d0">==</span> <span style="color:#90a959">'__main__'</span><span style="color:#d0d0d0">:</span>
    main<span style="color:#d0d0d0">()</span>
</code></span></span></span></span>

这段代码的开头三行引入了rclcpp和example_interfaces的依赖,然后定义了一个类MinimalService,继承自Node。MinimalService的构造函数中创建了一个服务,服务的类型是AddTwoInts,服务的名称是add_two_ints,回调函数是add_two_ints_callback。 add_two_ints_callback函数的功能是接收请求,计算结果,并返回响应。

MinimalService的main函数中创建了一个MinimalService的实例,并启动spin。spin的功能是不断地检查是否有可用的服务请求,如果有就调用回调函数。

2.7.2 创建client程序

这部分我们将代码写在一个名称叫做client_member_function.py的文件中。文件中代码如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">import</span> <span style="color:#f4bf75">sys</span>

<span style="color:#aa759f">from</span> <span style="color:#f4bf75">example_interfaces.srv</span> <span style="color:#aa759f">import</span> AddTwoInts
<span style="color:#aa759f">import</span> <span style="color:#f4bf75">rclpy</span>
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">rclpy.node</span> <span style="color:#aa759f">import</span> Node


<span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalClientAsync</span><span style="color:#d0d0d0">(</span>Node<span style="color:#d0d0d0">):</span>

    <span style="color:#aa759f">def</span> __init__<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        super<span style="color:#d0d0d0">().</span>__init__<span style="color:#d0d0d0">(</span><span style="color:#90a959">'minimal_client_async'</span><span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>cli <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>create_client<span style="color:#d0d0d0">(</span>AddTwoInts<span style="color:#d0d0d0">,</span> <span style="color:#90a959">'add_two_ints'</span><span style="color:#d0d0d0">)</span>
        <span style="color:#aa759f">while</span> <span style="color:#d0d0d0">not</span> self<span style="color:#d0d0d0">.</span>cli<span style="color:#d0d0d0">.</span>wait_for_service<span style="color:#d0d0d0">(</span>timeout_sec<span style="color:#d0d0d0">=</span><span style="color:#90a959">1.0</span><span style="color:#d0d0d0">):</span>
            self<span style="color:#d0d0d0">.</span>get_logger<span style="color:#d0d0d0">().</span>info<span style="color:#d0d0d0">(</span><span style="color:#90a959">'service not available, waiting again...'</span><span style="color:#d0d0d0">)</span>
        self<span style="color:#d0d0d0">.</span>req <span style="color:#d0d0d0">=</span> AddTwoInts<span style="color:#d0d0d0">.</span>Request<span style="color:#d0d0d0">()</span>

    <span style="color:#aa759f">def</span> send_request<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">,</span> a<span style="color:#d0d0d0">,</span> b<span style="color:#d0d0d0">):</span>
        self<span style="color:#d0d0d0">.</span>req<span style="color:#d0d0d0">.</span>a <span style="color:#d0d0d0">=</span> a
        self<span style="color:#d0d0d0">.</span>req<span style="color:#d0d0d0">.</span>b <span style="color:#d0d0d0">=</span> b
        self<span style="color:#d0d0d0">.</span>future <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>cli<span style="color:#d0d0d0">.</span>call_async<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">.</span>req<span style="color:#d0d0d0">)</span>
        rclpy<span style="color:#d0d0d0">.</span>spin_until_future_complete<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">,</span> self<span style="color:#d0d0d0">.</span>future<span style="color:#d0d0d0">)</span>
        <span style="color:#aa759f">return</span> self<span style="color:#d0d0d0">.</span>future<span style="color:#d0d0d0">.</span>result<span style="color:#d0d0d0">()</span>


<span style="color:#aa759f">def</span> main<span style="color:#d0d0d0">():</span>
    rclpy<span style="color:#d0d0d0">.</span>init<span style="color:#d0d0d0">()</span>

    minimal_client <span style="color:#d0d0d0">=</span> MinimalClientAsync<span style="color:#d0d0d0">()</span>
    response <span style="color:#d0d0d0">=</span> minimal_client<span style="color:#d0d0d0">.</span>send_request<span style="color:#d0d0d0">(</span>int<span style="color:#d0d0d0">(</span>sys<span style="color:#d0d0d0">.</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">1</span><span style="color:#d0d0d0">]),</span> int<span style="color:#d0d0d0">(</span>sys<span style="color:#d0d0d0">.</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">2</span><span style="color:#d0d0d0">]))</span>
    minimal_client<span style="color:#d0d0d0">.</span>get_logger<span style="color:#d0d0d0">().</span>info<span style="color:#d0d0d0">(</span>
        <span style="color:#90a959">'Result of add_two_ints: for %d + %d = %d'</span> <span style="color:#d0d0d0">%</span>
        <span style="color:#d0d0d0">(</span>int<span style="color:#d0d0d0">(</span>sys<span style="color:#d0d0d0">.</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">1</span><span style="color:#d0d0d0">]),</span> int<span style="color:#d0d0d0">(</span>sys<span style="color:#d0d0d0">.</span>argv<span style="color:#d0d0d0">[</span><span style="color:#90a959">2</span><span style="color:#d0d0d0">]),</span> response<span style="color:#d0d0d0">.</span>sum<span style="color:#d0d0d0">))</span>

    minimal_client<span style="color:#d0d0d0">.</span>destroy_node<span style="color:#d0d0d0">()</span>
    rclpy<span style="color:#d0d0d0">.</span>shutdown<span style="color:#d0d0d0">()</span>


<span style="color:#aa759f">if</span> __name__ <span style="color:#d0d0d0">==</span> <span style="color:#90a959">'__main__'</span><span style="color:#d0d0d0">:</span>
    main<span style="color:#d0d0d0">()</span>
</code></span></span></span></span>

这段代码比较简洁。照例是使用example_interfaces和rclpy的api.初始化,接着启用客户端发送请求,最后打印结果,最后销毁节点并关闭rclpy。具体代码不再一一分析。请查看入门教程介绍。

2.7.3 修改entry point,并编译

现在我们来修改setup.py并添加entry point,新增内容:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>entry_points<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">{</span>
    <span style="color:#90a959">'console_scripts'</span><span style="color:#d0d0d0">:</span> <span style="color:#d0d0d0">[</span>
        <span style="color:#90a959">'service = py_srvcli.service_member_function:main'</span><span style="color:#d0d0d0">,</span>
        <span style="color:#90a959">'client = py_srvcli.client_member_function:main'</span><span style="color:#d0d0d0">,</span>
    <span style="color:#d0d0d0">],</span>
<span style="color:#d0d0d0">},</span>
</code></span></span></span></span>

然后检查依赖项并编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> py_srvcli
Starting <span style="color:#d0d0d0">>>></span> py_srvcli
<span style="color:#f4bf75">---</span> stderr: py_srvcli                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> py_srvcli <span style="color:#d0d0d0">[</span>0.78s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.39s]
  1 package had stderr output: py_srvcli
</code></span></span></span></span>
2.7.4 运行测试

在一个终端运行client:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
<span style="color:#888888">## 确保package可见</span>
$ ros2 pkg list | grep py_srvcli
py_srvcli
<span style="color:#888888">## 查询可执行程序</span>
$ ros2 pkg executables py_srvcli
py_srvcli client
py_srvcli service
<span style="color:#888888">## 运行client</span>
$ ros2 run py_srvcli client 78 10
</code></span></span></span></span>

请注意,理论上服务器要先运行。这里让client运行是为了验证client程序的等待过程是否会报错。我们可以故意等几秒钟再启动server:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
<span style="color:#888888">## 运行service</span>
$ ros2 run py_srvcli service
</code></span></span></span></span>
2.7.5 意外

在上面测试的过程中我不小心将service = py_srvcli.service_member_function:main写成了servive = py_srvcli.service_member_fuction:main,导致程序无法运行。后来我修正之后,尝试重新编译之后程序出现了三个可执行程序:clientserviveservice。我甚至使用了colcon build --cmake-clean-first也没有效果。最后我索性删除build,install和log目录。然后重新编译。最后就可以了。(变成了只有两个可执行程序。)

2.7.6 总结

相比于2.6,可以发现2.7中的python程序本身更加简洁。流程大同小异。setup.py和CMakeLists.txt文件在配置execute point上有相同的作用。

在这一节我们了解到了rclpy中的Node::create_service创建服务器和使用Node::create_client创建客户端;也学会使用wait_for_service去等待服务器的可用性;也学会使用spin_until_future_complete函数去等待任务完成。

2.8 编写定制化的msg和srv文件

本小节参照入门教程Creating custom msg and srv files的内容。

在2.6和2.7中我们使用了examples_interfaces中的srv文件。但是我们也可以自己编写srv文件。(如果不能支持也太奇怪了。)所以本章将编写service(服务)需要的srv文件。我们在2.4和2.5中实现topic程序的时候使用的std_msgs库中的标准string格式的消息。其实还可一通过msg文件定制自己需要的消息。我们也在这一小节介绍。

本小节内容分为创建自定义接口和接口测试部分。篇幅较大,所以中间个别之前讲过的部分就会省略掉。因为本小节内容涉及接口部分,是一个比较综合的测试。所以本小节的文件夹明明就不以demo+数字命名。而是直接叫做custom_if_ws. 我们首先建立这个目录:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 首先导航到你放置练习工程的目录</span>
$ mkdir <span style="color:#f4bf75">-p</span> custom_if_ws/src
<span style="color:#888888">## 然后进入目录</span>
$ cd custom_if_ws
</code></span></span></span></span>
2.8.1 IDL(https://www.omg.org/spec/IDL/)

在开始正文之前,我们先来点理论的。本部分内容参考了IDL、IDL Mapping和About-Internal-Interfaces的部分内容。

IDL是OMG组织定义的一门描述性语言。我们来看一下OMG组织是如何定义IDL语言的:

IDL is a descriptive language used to define data types and interfaces in a way that is independent of the programming language or operating system/processor platform. The IDL specifies only the syntax used to define the data types and interfaces. It is normally used in connection with other specifications that further define how these types/interfaces are utilized in specific contexts and platforms.

IDL 是一种描述性语言,用于以独立于编程语言或操作系统/处理器平台的方式定义数据类型和接口。 IDL 仅指定用于定义数据类型和接口的语法。 它通常与其他规范结合使用,进一步定义如何在特定上下文和平台中使用这些类型/接口。

你可能会问了,我们现在正在讲srv和msg,这和IDL有什么关系呐?那是因为DDS使用IDL定义的数据格式。而DDS正是ROS2的核心通信组件。因此ROS2当然和IDL有关,而且IDL对于ROS2很重要。目前IDL语言最新的标准是2018年定义的4.2版本。ROS2支持了IDL 4.2版本的子集。如果你有打开idl文件的必要,有很多支持vscode的IDL解析插件,比如RTL开发的“OMG IDL”,当然还有更多。用户定义自己的msg和srv接口之后(表达了用户的意图),需要通过某种方式让ros2能够按照这个接口去通讯(ROS2底层需要理解用户的意图),这时候就出现了一套rosidl的工具来帮助翻译msg,srv(还有后面的action)文件成idl格式的数据接口。(实际上,整个过程可能更复杂。我会在第三节中再仔细将这部分的内容。)我们在这一节(即2.8节)中的主要工作就是掌握这些方法。

ROS2支持的IDL的子集,大家可以查看这个链接IDL MApping.这里简单罗列如下:

  • 注释(Comments): 行注释 (//) 和块注释(/* … */)都支持。
  • 标识符(Identifiers):标识符必须以 ASCII 字母字符开头,后跟任意数量的 ASCII 字母、数字和下划线(_)字符。
  • 字面值(Literals):整形(Integer),字符(Character),字符串(String),浮点数(Floating-point)和定点数(Fixed-point)。
  • 预处理(Preprocessing):目前,读取“.idl”文件时不会进行任何预处理。
  • 基本数据类型(Basic Types):整形(short,long,long long,unsigned short,unsigned long,unsigned long long, int8, uint8, int16, uint16, int32, uint32, int64, uint64), 浮点数(float, double, long double), 字符(char, wchar), 布尔数(boolean), 8进制数(octet).
  • 模板类型(Template Types):Sequences(sequence, sequence<type_spec, N>), string,wstring
  • 结构化类型:结构体(Structures), 枚举(Enumerations)和数组(Arrays)

下面描述一下IDL和其他语言的基础类型的映射关系: IDL type | C type | C++ type | Python type —|—|—|— float | float | float | float double | double | double | float long double | long double | long double2 | float char | unsigned char | unsigned char | str with length 1 wchar | char16_t | char16_t | str with length 1 boolean | _Bool | bool | bool octet | unsigned char | std::byte1 | bytes with length 1 int8 | int8_t | int8_t | int uint8 | uint8_t | uint8_t | int int16 | int16_t | int16_t | int uint16 | uint16_t | uint16_t | int int32 | int32_t | int32_t | int uint32 | uint32_t | uint32_t | int int64 | int64_t | int64_t | int uint64 | uint64_t | uint64_t | int

下面描述一下IDL和其他语言的复杂类型的映射关系: IDL type | C type | C++ type | Python type —|—|—|— T[N] | T[N] | std::array<T, N> | list sequence<T> | struct {size_t, T * } | std::vector<T> | list sequence<T, N> | struct {size_t, T * }, size_t N | std::vector<T> | list string | char * | std::string | str string<N> | char * | std::string | str wstring | char16_t * | std::u16string | str wstring<N> | char16_t * | std::u16string | str

以上仅仅是部分摘录,而且不一定准确。建议阅读原文。

为了便于理解整个过程,我将这个过程的示意图转载如下: 

rosidl动态消息类型

图9:rrosidl动态消息类型

这里需要将一个概念:metadata,元数据。元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。(引用自百度百科,我觉得这种说法还是有道理的。)我们在下文编写的src,msg文件本质是一个元数据文件。

2.8.2 创建自定义msg和srv接口

按照入门教程的描述,自己制作的msg和srv文件是需要放在一个单独的ament_cmake工程中,借助rosidl的一些工具来生成代码。因此我们要创建一个ament_cmake类型的package,包的名称叫做tutorial_interfaces:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg create tutorial_interfaces <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--destination-dir</span> src/ <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"A tutorial package with custom msg and srv interfaces"</span>
<span style="color:#888888">## 然后创建两个文件夹</span>
$ mkdir src/tutorial_interfaces/msg  src/tutorial_interfaces/srv
$ tree src/tutorial_interfaces/
src/tutorial_interfaces/
├── CMakeLists.txt
├── include
│   └── tutorial_interfaces
├── LICENSE
├── msg
├── package.xml
├── src
└── srv

5 directories, 3 files
<span style="color:#888888">## 使用vscode去编辑代码</span>
$ code src/tutorial_interfaces/
</code></span></span></span></span>

按照教程在msg中创建名为Num.msg的文件,内容填写为:

int64 num

请注意文件命名的时候单词首字母大写,这应该是一种规范。(具体原因暂时不知道,不大写应该也没有问题。只是如果你使用ros2 interface list去查看所有的接口的时候,你会发现接口的BaseName的命名都是字母首字母大写,比如geometry_msgs/msg/PoseArray。)这个文件应该也可以理解用来发布订阅的消息中包含一个名字叫num的int64类型的变量。

接着继续创建另一个名为Sphere.msg的msg文件,内容填写为:

geometry_msgs/Point center
float64 radius

通过ros2 interface list确实能够查看到这个类型(名称是geometry_msgs/msg/Pose):

$ ros2 interface show geometry_msgs/msg/Pose
# A representation of pose in free space, composed of position and orientation.

Point position
	float64 x
	float64 y
	float64 z
Quaternion orientation
	float64 x 0
	float64 y 0
	float64 z 0
	float64 w 1
$ ros2 interface show geometry_msgs/msg/Point
# This contains the position of a point in free space
float64 x
float64 y
float64 z

可以看出geometry_msgs/Point其实就是一个三个float64组成的矢量。 而要定义的Sphere除了这个点之外还定义了半径radius,半径也是float64的。

我们接着在定义一个名字叫AddThreeInts.srv的srv文件,既然是srv就需要两组数据,一组是请求的(request)另一组是响应的(response).srv的表示也非常简洁,用一行---隔开了上边的rquest和下边的response。如下:

int64 a
int64 b
int64 c
---
int64 sum

这个文件和example_interfaces的AddTwoInts.srv类似。只是2个请求参数变成了这里的3个。

编辑好这3个文件之后,我们就需要来编辑依赖项和CMake配置等信息。 这一节需要用到geometry_msgs和rosidl_default_generators这两个依赖项。前者在Sphere.msg中用到了,后者是rosidl转换需要的依赖项。要添加内容如下:

find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "msg/Sphere.msg"
  "srv/AddThreeInts.srv"
  DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)

注意真个文件没有必要添加add_executable标签。rosidl_generate_interfaces的标签的格式请注意。

接着修改package.xml文件。它的编写比我们之前在格式都复杂一些:

<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

通用依赖项(即在build,execute,test阶段都用到的依赖项)是geometry_msgs。rosidl_default_generators只能在build的时候用到,而rosidl_default_runtime只能在execute的时候用到。member_of_group教程说的是依赖组的名称。这一点我也没怎么理解。但whatever,就这么着吧。后面看看能不能理解。

好了,现在我们准备编译吧:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build  <span style="color:#f4bf75">--packages-select</span> tutorial_interfaces
<span style="color:#d0d0d0">[</span>0.757s] WARNING:colcon.colcon_core.prefix_path.colcon:The path <span style="color:#90a959">'/home/galileo/Workspaces/ROS2/execises/python_ws/install'</span> <span style="color:#aa759f">in </span>the environment variable COLCON_PREFIX_PATH doesn<span style="color:#90a959">'t exist
[0.757s] WARNING:colcon.colcon_ros.prefix_path.ament:The path '</span>/home/galileo/Workspaces/ROS2/execises/python_ws/install/py_srvcli<span style="color:#90a959">' in the environment variable AMENT_PREFIX_PATH doesn'</span>t exist
Starting <span style="color:#d0d0d0">>>></span> tutorial_interfaces
Finished <span style="color:#d0d0d0"><<<</span> tutorial_interfaces <span style="color:#d0d0d0">[</span>3.04s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>3.65s]
</code></span></span></span></span>

简单测试一下吧:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 interface list | grep <span style="color:#f4bf75">-E</span> <span style="color:#90a959">'Num|Sphere|AddThreeInts'</span>
    tutorial_interfaces/msg/Num
    tutorial_interfaces/msg/Sphere
    tutorial_interfaces/srv/AddThreeInts
$ ros2 interface show tutorial_interfaces/msg/Num
int64 num
$ ros2 interface show geometry_msgs/msg/Point
<span style="color:#888888"># This contains the position of a point in free space</span>
float64 x
float64 y
float64 z
$ ros2 interface show tutorial_interfaces/srv/AddThreeInts
int64 a
int64 b
int64 c
<span style="color:#f4bf75">---</span>
int64 sum
</code></span></span></span></span>

这和我们的设置一致。说明我们的接口设置完成。但是这些接口到底好不好用呐?我们在下文再做测试。

2.8.3 测试前准备

入门教程里面是将前几课的教程放在一个叫做ros_ws的文件夹。所以它们可以直接就在原来的工作空间操作。但是我们每一刻都是一个独立的工作空间。你可能就要说:你是不是傻眼了?那倒不至于。其实我当时将教程分开是考虑到后面的教程会修改前面的代码。这样独立开来会有很多好处。现在的解决方案也异常简单:直接拷贝一份过来修改成我们需要的代码就可以。(请注意我的demo3_ws、demo4_ws和本节的custom_if_ws都在同一个目录下。)我们可以这样将需要的工程拷贝过来就成,我们以2.4节的cpp_pubsub为例:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ cp <span style="color:#f4bf75">-r</span> ../demo3_ws/src/cpp_pubsub/ src/
</code></span></span></span></span>

接着我们索性将2.5,2.6和2.7节的package全部拷贝过来(不是剪切):

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ cp <span style="color:#f4bf75">-r</span> ../demo4_ws/src/py_pubsub/ src/
$ cp <span style="color:#f4bf75">-r</span> ../demo5_ws/src/cpp_srvcli/ src/
$ cp <span style="color:#f4bf75">-r</span> ../demo6_ws/src/py_srvcli/ src/
$ ls src/
cpp_pubsub  cpp_srvcli  py_pubsub  py_srvcli  tutorial_interfaces
</code></span></span></span></span>

这样我们就将原来的package源代码都拷贝过来了。接下来几个小节我们分别修改topic和service的示例工程,然后编译运行测试。

2.8.3 cpp_pubsub使用tutorial_interfaces/msg/Num消息接口

这一节我们下来使用cpp_pubsub来测试Num.msg接口。

先来看publisher_member_function.cpp这个文件的修改。这个文件的功能是发布消息。原来的教程使用的是std_msgs::msg::String这种类型来传递数据,这一次我们将使用2.8.1节中创建的tutorial_interfaces::msg::Num接口来传递数据。具体修改见下图: 

rosidl动态消息类型

图10:publisher_member_function代码差异

下面是修改总结:

  1. std_msgs/msg/string.hpp 替换为 tutorial_interfaces/msg/num.hpp
  2. create_publisher的模板由<std_msgs::msg::String>改为<tutorial_interfaces::msg::Num>。
  3. 回调的消息有String变成了Num。RCLCPP_INFO函数被更改为了RCLCPP_INFO_STREAM。

现在我们再来看subscriber_member_function.cpp这个文件的修改。这个文件的功能是订阅消息。原来的教程使用的是std_msgs::msg::String这种类型来接收数据,这一次我们将使用2.8.1节中创建的tutorial_interfaces::msg::Num接口来接收数据。具体修改见下图: 

rosidl动态消息类型

图11:subscriber_member_function代码差异

下面是修改总结:

  1. std_msgs/msg/string.hpp 替换为 tutorial_interfaces/msg/num.hpp
  2. create_subscription的模板由<std_msgs::msg::String>改为<tutorial_interfaces::msg::Num>。
  3. 回调的消息有String变成了Num。RCLCPP_INFO函数被更改为了RCLCPP_INFO_STREAM。

因为依赖项由之前的std_msgs变成了tutorial_interfaces。所以CMakeLists.txt和package.xml也需要修改。请按照入门教程修改,这里不再赘述。基本上是将std_msgs替换为tutorial_interfaces。或者添加tutorial_interfaces依赖。因为tutorial_interfaces本质是依赖于std_msgs的。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/cpp_pubsub  <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
ERROR: the following packages/stacks could not have their rosdep keys resolved
to system dependencies:
cpp_pubsub: Cannot locate rosdep definition <span style="color:#aa759f">for</span> <span style="color:#d0d0d0">[</span>tutorial_interfaces]
</code></span></span></span></span>

怎么报错了呐?

原来我还没有source当前的overlay。(因为别的原因我关闭了之前的终端。)而我们当前需要编译的源码依赖于tutorial_interfaces,所以我们必须先将tutorial_interfaces配置到当前的环境中才可以。所以我们重来一次:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/cpp_pubsub  <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> cpp_pubsub
Starting <span style="color:#d0d0d0">>>></span> cpp_pubsub
Finished <span style="color:#d0d0d0"><<<</span> cpp_pubsub <span style="color:#d0d0d0">[</span>5.84s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>6.46s]
<span style="color:#888888">## 我们新增加了包到环境中,所以现在需要重新source一次overlay</span>
$ source install/setup.sh
<span style="color:#888888">## 检查一下cpp_pubsub是否加载成功</span>
$ ros2 pkg executables cpp_pubsub
cpp_pubsub listener
cpp_pubsub talker
$ ros2 run cpp_pubsub listener
</code></span></span></span></span>

现在我们打开第二个终端(如果你也像我一样使用tmux,那这时候你可以将现在的终端分成两个窗口来避免在新打开的终端里面重新切换到目标目录并source):

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 先cd到本节课程的工作空间</span>
<span style="color:#888888">## 然后source</span>
$ source install/setup.sh
$ ros2 run cpp_pubsub talker
</code></span></span></span></span>

结果如下: 

cpp_pubsub使用Num传递消息测试

图12:cpp_pubsub使用Num传递消息测试

2.8.4 py_pubsub使用tutorial_interfaces/msg/Num消息接口

这一节我们下来使用py_pubsub来测试Num.msg接口。

按照入门教程改动即可,还是修改头文件并将std_msgs/msg/String更改为tutorial_interfaces/msg/Num。这里就不再赘述。

ament_python的修改比较简单,仅修改package.xml文件即可。只需要中的exec_depend标签内的std_msgs更改为tutorial_interfaces即可。

然后按照之前的流程编译即可:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/py_pubsub  <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span>  py_pubsub
</code></span></span></span></span>

然后在这个窗口运行lisenter:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
<span style="color:#888888">## 检查一下py_pubsub是否加载成功</span>
$ ros2 pkg executables py_pubsub
py_pubsub listener
py_pubsub talker
$ ros2 run py_pubsub listener
</code></span></span></span></span>

在另一个终端运行talker:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ ros2 run py_pubsub talker
</code></span></span></span></span>

运行结果和2.8.3节一样。不再赘述。

2.8.5 cpp_srvcli使用tutorial_interfaces/srv/AddThreeInts服务接口

cpp_srvcli之前的两个源文件是add_two_ints_server.cppadd_two_ints_client.cpp。为了名副其实我们将它们分别更名为add_three_ints_server.cppadd_three_ints_client.cpp

在add_two_ints_server.cpp中使用的是example_interfaces::srv::AddTwoInts,而我们这次使用的是tutorial_interfaces/srv/AddThreeInts服务接口。所以要做相应的替换,而之前request的数据也在a和b之外添加了c.当然头文件也要相应修改,但是请注意头文件的basename变成了全小写。我们就按照这个原则修改add_three_ints_server.cpp文件。同样的方法用到add_three_ints_client.cpp的修改中。只是请注意这次argc变成了4.

CMakeLists.txt文件除了依赖项要从example_interfaces改为tutorial_interfaces之外,还要更改目标文件的名字。(入门教程没有更改文件名称)

package.xml只需要将依赖项从example_interfaces改为tutorial_interfaces即可。

然后编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/cpp_srvcli  <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> cpp_srvcli
Starting <span style="color:#d0d0d0">>>></span> cpp_srvcli
Finished <span style="color:#d0d0d0"><<<</span> cpp_srvcli <span style="color:#d0d0d0">[</span>2.12s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>2.75s]
<span style="color:#888888">## 我们新增加了包到环境中,所以现在需要重新source一次overlay</span>
$ source install/setup.sh
<span style="color:#888888">## 检查一下cpp_srvcli是否加载成功</span>
$ ros2 pkg executables cpp_srvcli
cpp_srvcli client
cpp_srvcli server
$ ros2 run cpp_srvcli client 11 22 33
</code></span></span></span></span>

在另一个终端延迟打开server:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ ros2 run cpp_srvcli server
</code></span></span></span></span>

测试结果如下: 

AddThreeInts传递服务消息测试

图13:AddThreeInts传递服务消息测试

2.8.6 py_srvcli使用tutorial_interfaces/srv/AddThreeInts服务接口

py_srvcli的测试和cpp_srvcli大同小异(程序更简单)。细节不再赘述。参见入门教程即可。

然后编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/py_srvcli  <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> py_srvcli
Starting <span style="color:#d0d0d0">>>></span> py_srvcli
<span style="color:#f4bf75">---</span> stderr: py_srvcli                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> py_srvcli <span style="color:#d0d0d0">[</span>0.84s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.46s]
  1 package had stderr output: py_srvcli
<span style="color:#888888">## 我们新增加了包到环境中,所以现在需要重新source一次overlay</span>
$ source install/setup.sh
<span style="color:#888888">## 检查一下py_srvcli是否加载成功</span>
$ ros2 pkg executables py_srvcli
py_srvcli client
py_srvcli service
$ ros2 run py_srvcli client 11 22 33
</code></span></span></span></span>

在另一个终端延迟打开server:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.sh
$ ros2 run py_srvcli service
</code></span></span></span></span>

测试完成。

2.8.7 总结

2.8节学会了怎么使用自定义的srv,msg接口。我们也初步接触了rosidl.未来将继续研究ros2的API.

2.9 实现自定义接口

本小节参照入门教程Implementing custom interfaces的内容。

在上一节我们使用了专门的包来创建接口。这是ROS官方推荐的做法,但是你也看到整个过程比较繁琐。你需要先准备好接口并将其当作被使用接口的underlay才能够支持后者编译和运行。那有没有简单的做法。本章就描述了这种方法。

因为接口文件只能在ament_cmake中编译,所以如果你当你需要访问包内自定义接口的时候,你的工程必须是ament_cmake。如果你必须使用python,那有两种方法:一种是做成独立的包,另一种将python封装成可供cpp使用的库。为简单期间,入门教程介绍了在ament_cmake包内使用自定义接口的办法。

2.9.1 实现基础接口

本节教程我们还在custom_if_ws工作区中操作。我们首先创建一个名称为more_interfaces,类型是ament_cmake的包:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 确保已经cd到了custom_if_ws目录中</span>
$ ros2 pkg create more_interfaces <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--destination-directory</span> src/ <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"A package with custom interfaces"</span>
$ ls src/
cpp_pubsub  cpp_srvcli  more_interfaces  py_pubsub  py_srvcli  tutorial_interfaces
<span style="color:#888888">## 照例创建一个msg文件夹用来存放消息接口描述文件</span>
$ mkdir src/more_interfaces/msg
<span style="color:#888888">## 我们顺便打开vscode</span>

</code></span></span></span></span>

现在我们将要创建一个名为AddressBook.msg的消息描述文件,内容如下:

uint8 PHONE_TYPE_HOME=0
uint8 PHONE_TYPE_WORK=1
uint8 PHONE_TYPE_MOBILE=2

string first_name
string last_name
string phone_number
uint8 phone_type

这个文件相比于2.8中的定义稍微复杂了一点。前三行定义了三个常量。后面定义了三个string类型的变量分别表示姓、名、手机号。然后定义了一个uint8_t类型的变量表示手机类型。只是三个常量看起来表示phone_type。但是具体将这几个常量制定给phone_type这个变量,我尚不清楚。在之前Baisc/Interface的篇幅讲了接口定义的内容。只是因为当时对接口还不太理解。还不知道原来接口的作用这么大。(之前不知道接口可以自定义。)

我们照例要修改依赖项,在2.8节已经描述过了(rosidl的编译、运行相关的依赖项)。在package.xml中添加:

<buildtool_depend>rosidl_default_generators</buildtool_depend>

<exec_depend>rosidl_default_runtime</exec_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

在CMakeLists.txt中添加:

find_package(rosidl_default_generators REQUIRED)


rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/AddressBook.msg"
)

此外还需要额外添加一条(相比于2.8节):

$ rosdep install -i --from-paths src/more_interfaces/  --rosdistro humble -y
#All required rosdeps installed successfully
$ colcon build --packages-select more_interfaces
Starting >>> more_interfaces
Finished <<< more_interfaces [2.25s]                     

Summary: 1 package finished [2.86s]

有意思的是上文CMakeLists.txt其实可以这样编写:

find_package(rosidl_default_generators REQUIRED)

set(msg_files
  "msg/AddressBook.msg"
)

rosidl_generate_interfaces(${PROJECT_NAME}
  $(msg_files)
)

我们甚至可以将上面的文件扩展为一种常规结构:

find_package(rosidl_default_generators REQUIRED)

set(msg_files
  "msg/AddressBook.msg"
)

set(srv_files
  "srv/Service#.src"
)

set(action_files
  "action/Action#.src"
)

rosidl_generate_interfaces(${PROJECT_NAME}
  $(msg_files)
  $(srv_files)
  $(action_files)
)

[上文是举例,你需要根据你世纪的文件来扩展上述语法。]

然后执行依赖项检查和编译工作。最后我们开测试一下接口:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 interface list | grep more
    more_interfaces/msg/AddressBook
$ ros2 interface show more_interfaces/msg/AddressBook
uint8 PHONE_TYPE_HOME<span style="color:#d0d0d0">=</span>0
uint8 PHONE_TYPE_WORK<span style="color:#d0d0d0">=</span>1
uint8 PHONE_TYPE_MOBILE<span style="color:#d0d0d0">=</span>2

string first_name
string last_name
string phone_number
uint8 phone_type
</code></span></span></span></span>
2.9.2 创建使用接口的代码

现在我们来创建使用我们在2.9.1中创建的AddressBook消息接口。

我们先创建一个文件名字叫做publish_address_book.cpp,内容如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include <chrono>
#include <memory>
</span>
<span style="color:#f4bf75">#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"
</span>
<span style="color:#aa759f">using</span> <span style="color:#aa759f">namespace</span> std<span style="color:#d0d0d0">::</span>chrono_literals<span style="color:#d0d0d0">;</span>

<span style="color:#aa759f">class</span> <span style="color:#f4bf75">AddressBookPublisher</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> rclcpp<span style="color:#d0d0d0">::</span>Node
<span style="color:#d0d0d0">{</span>
public:
  AddressBookPublisher<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">:</span> Node<span style="color:#d0d0d0">(</span><span style="color:#90a959">"address_book_publisher"</span><span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    address_book_publisher_ <span style="color:#d0d0d0">=</span>
      <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_publisher<span style="color:#d0d0d0"><</span>more_interfaces<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>AddressBook<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">(</span><span style="color:#90a959">"address_book"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">10</span><span style="color:#d0d0d0">);</span>

    <span style="color:#aa759f">auto</span> publish_msg <span style="color:#d0d0d0">=</span> <span style="color:#d0d0d0">[</span><span style="color:#aa759f">this</span><span style="color:#d0d0d0">]()</span> <span style="color:#d0d0d0">-></span> <span style="color:#d28445">void</span> <span style="color:#d0d0d0">{</span>
        <span style="color:#aa759f">auto</span> message <span style="color:#d0d0d0">=</span> more_interfaces<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>AddressBook<span style="color:#d0d0d0">();</span>

        message<span style="color:#d0d0d0">.</span>first_name <span style="color:#d0d0d0">=</span> <span style="color:#90a959">"John"</span><span style="color:#d0d0d0">;</span>
        message<span style="color:#d0d0d0">.</span>last_name <span style="color:#d0d0d0">=</span> <span style="color:#90a959">"Doe"</span><span style="color:#d0d0d0">;</span>
        message<span style="color:#d0d0d0">.</span>phone_number <span style="color:#d0d0d0">=</span> <span style="color:#90a959">"1234567890"</span><span style="color:#d0d0d0">;</span>
        message<span style="color:#d0d0d0">.</span>phone_type <span style="color:#d0d0d0">=</span> message<span style="color:#d0d0d0">.</span>PHONE_TYPE_MOBILE<span style="color:#d0d0d0">;</span>

        std<span style="color:#d0d0d0">::</span>cout <span style="color:#d0d0d0"><<</span> <span style="color:#90a959">"Publishing Contact</span><span style="color:#8f5536">\n</span><span style="color:#90a959">First:"</span> <span style="color:#d0d0d0"><<</span> message<span style="color:#d0d0d0">.</span>first_name <span style="color:#d0d0d0"><<</span>
          <span style="color:#90a959">"  Last:"</span> <span style="color:#d0d0d0"><<</span> message<span style="color:#d0d0d0">.</span>last_name <span style="color:#d0d0d0"><<</span> std<span style="color:#d0d0d0">::</span>endl<span style="color:#d0d0d0">;</span>

        <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>address_book_publisher_<span style="color:#d0d0d0">-></span>publish<span style="color:#d0d0d0">(</span>message<span style="color:#d0d0d0">);</span>
      <span style="color:#d0d0d0">};</span>
    timer_ <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_wall_timer<span style="color:#d0d0d0">(</span><span style="color:#90a959">1s</span><span style="color:#d0d0d0">,</span> publish_msg<span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span>

private:
  rclcpp<span style="color:#d0d0d0">::</span>Publisher<span style="color:#d0d0d0"><</span>more_interfaces<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>AddressBook<span style="color:#d0d0d0">>::</span>SharedPtr address_book_publisher_<span style="color:#d0d0d0">;</span>
  rclcpp<span style="color:#d0d0d0">::</span>TimerBase<span style="color:#d0d0d0">::</span>SharedPtr timer_<span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">};</span>


<span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span> <span style="color:#d0d0d0">*</span> argv<span style="color:#d0d0d0">[])</span>
<span style="color:#d0d0d0">{</span>
  rclcpp<span style="color:#d0d0d0">::</span>init<span style="color:#d0d0d0">(</span>argc<span style="color:#d0d0d0">,</span> argv<span style="color:#d0d0d0">);</span>
  rclcpp<span style="color:#d0d0d0">::</span>spin<span style="color:#d0d0d0">(</span>std<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0"><</span>AddressBookPublisher<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">());</span>
  rclcpp<span style="color:#d0d0d0">::</span>shutdown<span style="color:#d0d0d0">();</span>

  <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

我们再来分析一下上面的代码。头文件部分照例引用了rclcpp接口,然后使用我们方才自定义的more_interfaces/msg/AddressBook消息接口的头文件more_interfaces/msg/address_book.hpp

整个程序的流程还是先初始化(init),然后循环调用(spin)自定义的类AddressBookPublisher的成员(使用std::make_shared创建类的实体),最后从循环中跳出后关闭(shutdown)程序。

AddressBookPublisher的构建程序中,先使用create_publisher创建一个名叫address_book_publisher_的发布者(重载类型为more_interfaces::msg::AddressBook)。然后使用Lambda表达式创建一个消息发布者publish_msg。publish_msg函数中,先创建一个AddressBook类型的消息,然后设置相关的字段,然后打印相关信息,最后使用address_book_publisher_的publish函数发布消息。最后使用create_wall_timer创建一个名为timer_的定时器按照1s的周期调用publish_msg函数发布消息。

我们不妨先编译一下publisher的代码。现在需要根据将新增的依赖项rclcpp添加到package.xml和CMakeLists.txt文件中。在package.xml中添加:

<depend>rclcpp</depend>

在CMakeLists.txt文件中添加:

find_package(rclcpp REQUIRED)

add_executable(publish_address_book src/publish_address_book.cpp)
ament_target_dependencies(publish_address_book rclcpp)

install(TARGETS
    publish_address_book
  DESTINATION lib/${PROJECT_NAME})

除了这些常规代码,还需要添加:

rosidl_get_typesupport_target(cpp_typesupport_target
  ${PROJECT_NAME} rosidl_typesupport_cpp)

target_link_libraries(publish_address_book "${cpp_typesupport_target}")

这些代码将告诉编译期要从相同的节点寻找自定义接口。这段代码也是区别与从其它package加载的最主要区别。请多加注意。

我们现在再次检查依赖项并编译。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/more_interfaces/ <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> more_interfaces
Starting <span style="color:#d0d0d0">>>></span> more_interfaces
Finished <span style="color:#d0d0d0"><<<</span> more_interfaces <span style="color:#d0d0d0">[</span>3.69s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>4.31s]
</code></span></span></span></span>

现在我们来测试它。我们将用ros2 topic echo来查看我们的发布的消息。之后真实代码的编写中也可以用这种方式调试Topic代码。 在一个终端调用publish_address_book去发布消息:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 run more_interfaces publish_address_book
</code></span></span></span></span>

在另一个终端调用ros2 topic echo来查看发布的消息:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 topic echo /address_book
</code></span></span></span></span>

需要注意如果我们在这个终端中不source的话,尽管我们可以通过ros2 topic list发现/address_book这个topic,但是我们无法查看发布的消息:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 topic echo /address_book
The message type <span style="color:#90a959">'more_interfaces/msg/AddressBook'</span> is invalid
</code></span></span></span></span>

所以必须事先source.

结果如下: 

AddressBook消息接口测试

图14:AddressBook消息接口测试

2.9.3 使用已存在的接口定义

入门教程的这一部分假定我们在自己的msg文件里面包含了其它包的相应文件。文中举的例子是名字叫做rosidl_tutorials_msgs的包。这个包实际上是不存在。它举出的例子我们在定义Sphere.msg的时候已经用过了。Sphere.msg的定义如下:

geometry_msgs/Point center
float64 radius

其中geometry_msgs/Point就是存在的另一个包。具体的流程可以参考这部分(2.8.2)即可。

我们在定义这个包的时候也在package.xml和CMakeLists.txt中添加了依赖项geometry_msgs。只是那个包中间没有引用geometry_msgs/msg/point.hpp文件.因为尽管这个Sphere这个消息体尽管被定义了。但是最终没有被使用。

2.9.4 总结

这一节我们继续深入的了解了如何在同一个package使用自定义的interface.但是这并不是官方推荐的一种方式。尤其是如果在python包中使用则会非常繁琐。(与其这样不如为你的工程专门制作制作一个基础的接口package.将你会用到的特殊接口全部定义在其中。这样可以方便管理和使用。)

2.10 在C++类中使用参数(Parameters)

本小节参照入门教程Using parameters in a class(C++)的内容。

我们在笔记1中 本节将学习如何在C++类中使用parameters。在开始之前我们不妨先建立一个基本的工程,随后再一步一步了解怎么在被工程中引用parameters,最后学会在launch中设置参数。

2.10.1 准备工作-创建基础工程

先创建一个demo7_ws工作区,然后创建一个cpp_parameters包。这个包的是ament_cmake类型;名字不妨叫做cpp_parameters;依赖项是rclcpp;license照例是Apache-2.0:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ mkdir <span style="color:#f4bf75">-p</span> demo7_ws/src
$ cd demo7_ws
$ ros2 pkg create cpp_parameters <span style="color:#f4bf75">--destination-directory</span> src/ <span style="color:#f4bf75">--build-type</span> ament_cmake  <span style="color:#f4bf75">--license</span> Apache-2.0  <span style="color:#f4bf75">--dependencies</span> rclcpp  <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"C++ parameter tutorial"</span>
$ tree  src/cpp_parameters/
src/cpp_parameters/
├── CMakeLists.txt
├── include
│   └── cpp_parameters
├── LICENSE
├── package.xml
└── src

3 directories, 3 files
$ code src/cpp_parameters
</code></span></span></span></span>

我了后面使用parameter,我们需要在即将编写的代码中包含一些pkg相关的参数。我们可以按照入门教程的代码,创建一个cpp_parameters_node.cpp文件,并填入代码:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include <chrono>
#include <functional>
#include <string>
</span>
<span style="color:#f4bf75">#include <rclcpp/rclcpp.hpp>
</span>
<span style="color:#aa759f">using</span> <span style="color:#aa759f">namespace</span> std<span style="color:#d0d0d0">::</span>chrono_literals<span style="color:#d0d0d0">;</span>

<span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalParam</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> rclcpp<span style="color:#d0d0d0">::</span>Node
<span style="color:#d0d0d0">{</span>
public:
  MinimalParam<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">:</span> Node<span style="color:#d0d0d0">(</span><span style="color:#90a959">"minimal_param_node"</span><span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>declare_parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">"my_parameter"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">"world"</span><span style="color:#d0d0d0">);</span>

    timer_ <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_wall_timer<span style="color:#d0d0d0">(</span>
      <span style="color:#90a959">1000ms</span><span style="color:#d0d0d0">,</span> std<span style="color:#d0d0d0">::</span>bind<span style="color:#d0d0d0">(</span><span style="color:#d0d0d0">&</span>MinimalParam<span style="color:#d0d0d0">::</span>timer_callback<span style="color:#d0d0d0">,</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">));</span>
  <span style="color:#d0d0d0">}</span>

  <span style="color:#d28445">void</span> timer_callback<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">{</span>
    std<span style="color:#d0d0d0">::</span>string my_param <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>get_parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">"my_parameter"</span><span style="color:#d0d0d0">).</span>as_string<span style="color:#d0d0d0">();</span>

    RCLCPP_INFO<span style="color:#d0d0d0">(</span><span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>get_logger<span style="color:#d0d0d0">(),</span> <span style="color:#90a959">"Hello %s!"</span><span style="color:#d0d0d0">,</span> my_param<span style="color:#d0d0d0">.</span>c_str<span style="color:#d0d0d0">());</span>

    std<span style="color:#d0d0d0">::</span>vector<span style="color:#d0d0d0"><</span>rclcpp<span style="color:#d0d0d0">::</span>Parameter<span style="color:#d0d0d0">></span> all_new_parameters<span style="color:#d0d0d0">{</span>rclcpp<span style="color:#d0d0d0">::</span>Parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">"my_parameter"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">"world"</span><span style="color:#d0d0d0">)};</span>
    <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>set_parameters<span style="color:#d0d0d0">(</span>all_new_parameters<span style="color:#d0d0d0">);</span>
  <span style="color:#d0d0d0">}</span>

private:
  rclcpp<span style="color:#d0d0d0">::</span>TimerBase<span style="color:#d0d0d0">::</span>SharedPtr timer_<span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">};</span>

<span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span> <span style="color:#d0d0d0">**</span> argv<span style="color:#d0d0d0">)</span>
<span style="color:#d0d0d0">{</span>
  rclcpp<span style="color:#d0d0d0">::</span>init<span style="color:#d0d0d0">(</span>argc<span style="color:#d0d0d0">,</span> argv<span style="color:#d0d0d0">);</span>
  rclcpp<span style="color:#d0d0d0">::</span>spin<span style="color:#d0d0d0">(</span>std<span style="color:#d0d0d0">::</span>make_shared<span style="color:#d0d0d0"><</span>MinimalParam<span style="color:#d0d0d0">></span><span style="color:#d0d0d0">());</span>
  rclcpp<span style="color:#d0d0d0">::</span>shutdown<span style="color:#d0d0d0">();</span>
  <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

上面的程序还是rclcpp传统的流程不再赘述,我们主要分析MinimalParam这个继承自Node的Class. 在MinimalParam的构造函数中使用Node::declare_parameter茶u年构建了一个key是”my_parameter”的参数,并为这个参数赋初值为“world”。然后创建一个定时器,每隔1s调用timer_callback函数。在timer_callback函数中,我们使用Node::get_parameter函数获取参数值,并打印出来。然后我们使用Node::set_parameters函数修改参数值。

通过这操作可以将外部设置的参数显示出来,显示完之后有将参数值设为默认值。

我们也可以在构造函数中添加参数描述符(param description),这样在使用ros2 param list命令时可以看到参数的描述。参数描述符(param description)除了能简单描述参数之外还可以给它添加一些限制,比如是否只读,参数范围等。这样我们相关的程序就可以修改为:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">// ...</span>
<span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalParam</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> rclcpp<span style="color:#d0d0d0">::</span>Node
<span style="color:#d0d0d0">{</span>
public:
  MinimalParam<span style="color:#d0d0d0">()</span>
  <span style="color:#d0d0d0">:</span> Node<span style="color:#d0d0d0">(</span><span style="color:#90a959">"minimal_param_node"</span><span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    <span style="color:#aa759f">auto</span> param_desc <span style="color:#d0d0d0">=</span> rcl_interfaces<span style="color:#d0d0d0">::</span>msg<span style="color:#d0d0d0">::</span>ParameterDescriptor<span style="color:#d0d0d0">{};</span>
    param_desc<span style="color:#d0d0d0">.</span>description <span style="color:#d0d0d0">=</span> <span style="color:#90a959">"This parameter is mine!"</span><span style="color:#d0d0d0">;</span>

    <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>declare_parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">"my_parameter"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">"world"</span><span style="color:#d0d0d0">,</span> param_desc<span style="color:#d0d0d0">);</span>

    timer_ <span style="color:#d0d0d0">=</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">-></span>create_wall_timer<span style="color:#d0d0d0">(</span>
      <span style="color:#90a959">1000ms</span><span style="color:#d0d0d0">,</span> std<span style="color:#d0d0d0">::</span>bind<span style="color:#d0d0d0">(</span><span style="color:#d0d0d0">&</span>MinimalParam<span style="color:#d0d0d0">::</span>timer_callback<span style="color:#d0d0d0">,</span> <span style="color:#aa759f">this</span><span style="color:#d0d0d0">));</span>
  <span style="color:#d0d0d0">}</span>
 <span style="color:#888888">// ......</span>
<span style="color:#d0d0d0">}</span> 
</code></span></span></span></span>

现在我们需要修改CMakeLists.txt文件,添加我们的目标文件。如下:

add_executable(minimal_param_node src/cpp_parameters_node.cpp)
ament_target_dependencies(minimal_param_node rclcpp)

install(TARGETS
    minimal_param_node
  DESTINATION lib/${PROJECT_NAME}
)

然后我们检查依赖项并编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-paths</span> src/cpp_parameters/ <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build <span style="color:#f4bf75">--packages-select</span> cpp_parameters
Starting <span style="color:#d0d0d0">>>></span> cpp_parameters
Finished <span style="color:#d0d0d0"><<<</span> cpp_parameters <span style="color:#d0d0d0">[</span>3.63s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>4.28s]
</code></span></span></span></span>

最后我们source一下环境变量,并运行程序:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 pkg list | grep cpp_parameters
cpp_parameters
$ ros2 run cpp_parameters minimal_param_node
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706097049.922609500] <span style="color:#d0d0d0">[</span>minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706097050.922285795] <span style="color:#d0d0d0">[</span>minimal_param_node]: Hello world!
<span style="color:#888888"># ...</span>
</code></span></span></span></span>

我们可以看到程序打印出了参数值。

2.10.2 从外部修改参数测试

不要关闭上一个终端,我们打开一个新的终端窗口:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888"># cd到你的demo7_ws工作区,然后source环境变量</span>
$ source install/setup.bash 
<span style="color:#888888">## cpp_parameters运行后方可参看参数</span>
$ ros2 param list
/minimal_param_node:
  my_parameter
  qos_overrides./parameter_events.publisher.depth
  qos_overrides./parameter_events.publisher.durability
  qos_overrides./parameter_events.publisher.history
  qos_overrides./parameter_events.publisher.reliability
  use_sim_time
<span style="color:#888888">## 获取节点名称,下一步需要  </span>
$ ros2 node list
/minimal_param_node
<span style="color:#888888">## 获取参数</span>
$ ros2 param get /minimal_param_node my_parameter
String value is: world
$ ros2 param set /minimal_param_node my_parameter earth/sky
</code></span></span></span></span>

测试如下: 

外部设置参数测试

图15:外部设置参数测试

对了别忘了使用ros2 param describe命令来查看参数的描述:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 param describe  minimal_param_node my_parameter
Parameter name: my_parameter
  Type: string
  Description: This parameter is mine!
  Constraints:
</code></span></span></span></span>

请注意要确保当节点运行后才能使用上面的命令查看参数。

2.10.3 从launch修改参数

通过param修改参数是一种临时修改,如果需要长期修改,则需要修改launch文件。我们可以修改launch文件,使得参数可以在launch文件中设置。

但是launch方式修改的流程相对复杂一些。我们现在来学习怎么使用launch文件修改参数吧。首先我们先在package目录下创建一个名字叫做launch文件夹:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 确保我们已经cd到了demo7_ws工作区;当然你也可以进入到cpp_parameters包目录下</span>
<span style="color:#888888">## 我的操作都假定目前正位于demo7_ws工作区目录下</span>
$ mkdir <span style="color:#f4bf75">-p</span> src/cpp_parameters/launch
<span style="color:#888888">## 我们照例在vscode里面打开cpp_parameters包目录</span>
$ code src/cpp_parameters
</code></span></span></span></span>

然后我们在launch文件夹下创建一个名字叫做cpp_parameters_launch.py的文件,内容如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">from</span> <span style="color:#f4bf75">launch</span> <span style="color:#aa759f">import</span> LaunchDescription
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">launch_ros.actions</span> <span style="color:#aa759f">import</span> Node

<span style="color:#aa759f">def</span> generate_launch_description<span style="color:#d0d0d0">():</span>
    <span style="color:#aa759f">return</span> LaunchDescription<span style="color:#d0d0d0">([</span>
        Node<span style="color:#d0d0d0">(</span>
            package<span style="color:#d0d0d0">=</span><span style="color:#90a959">"cpp_parameters"</span><span style="color:#d0d0d0">,</span>
            executable<span style="color:#d0d0d0">=</span><span style="color:#90a959">"minimal_param_node"</span><span style="color:#d0d0d0">,</span>
            name<span style="color:#d0d0d0">=</span><span style="color:#90a959">"custom_minimal_param_node"</span><span style="color:#d0d0d0">,</span>
            output<span style="color:#d0d0d0">=</span><span style="color:#90a959">"screen"</span><span style="color:#d0d0d0">,</span>
            emulate_tty<span style="color:#d0d0d0">=</span>True<span style="color:#d0d0d0">,</span>
            parameters<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">[</span>
                <span style="color:#d0d0d0">{</span><span style="color:#90a959">"my_parameter"</span><span style="color:#d0d0d0">:</span> <span style="color:#90a959">"earth"</span><span style="color:#d0d0d0">}</span>
            <span style="color:#d0d0d0">]</span>
        <span style="color:#d0d0d0">)</span>
    <span style="color:#d0d0d0">])</span>
</code></span></span></span></span>

这里需要说明一下,这里的package和executable名称均是我们在2.10.2中使用的名称。executable的获取我们可以使用ros2 pkg executables cpp_parameters来获取。我们设置的为name设置的custom_minimal_param_node,也会在后面launch测试的时候显示出来.你稍后可以留意。

我们接下来修改CMakeLists.txt文件,以确保setup脚本可以调用launch实现对参数的编辑.添加:

install(
  DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)

然后我们编译一下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ colcon build <span style="color:#f4bf75">--packages-select</span> cpp_parameters
Starting <span style="color:#d0d0d0">>>></span> cpp_parameters
Finished <span style="color:#d0d0d0"><<<</span> cpp_parameters <span style="color:#d0d0d0">[</span>0.71s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.31s]
</code></span></span></span></span>

好了现在我们开始测试吧。让我们先source一下环境变量:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>source install/setup.bash
</code></span></span></span></span>

接着使用launch命令执行刚才的cpp_parameters_launch.py的文件:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>ros2 launch cpp_parameters cpp_parameters_launch.py
</code></span></span></span></span>

测试结果如下: 

通过launch设置参数实验

图16:通过launch设置参数实验

扩展: 另外我发现,对于python文件编译的过程实际上是检查编译通过后之后将python文件copy到install/cpp_parameters/share/cpp_parameters/launch/的目录中。如果我们这时候修改一下这个python文件,比方我们将earth改成Mars,会发现:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 launch cpp_parameters cpp_parameters_launch.py
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>launch]: All log files can be found below /home/galileo/.ros/log/2024-01-25-12-23-16-861542-Galileo-Dell-Linux-23086
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>launch]: Default logging verbosity is set to INFO
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>minimal_param_node-1]: process started with pid <span style="color:#d0d0d0">[</span>23087]
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706156597.930575708] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello Mars!!
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706156598.930572550] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706156599.930544611] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello world!
</code></span></span></span></span>

2.11 在Python类中使用参数(Parameters)

本小节参照入门教程Using parameters in a class (Python)的内容。

2.11.1 创建基础包

我们在2.10中介绍了在C++中使用参数,这一节我们介绍在python中怎么使用paramters.我们先来准备一个包含paramter的python package吧。本次我们将创建一个名字叫demo8_ws的新工作区,并创建一个叫做python_parameters的ament_python类型的package:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ mkdir <span style="color:#f4bf75">-p</span> demo8_ws/src
$ cd demo8_ws
$ ros2 pkg create python_parameters <span style="color:#f4bf75">--destination-directory</span> src/ <span style="color:#f4bf75">--build-type</span> ament_python  <span style="color:#f4bf75">--license</span> Apache-2.0  <span style="color:#f4bf75">--dependencies</span> rclpy  <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"Python parameter tutorial"</span>
$ tree pkg src/python_parameters/
pkg  <span style="color:#d0d0d0">[</span>error opening dir<span style="color:#d0d0d0">]</span>
src/python_parameters/
├── LICENSE
├── package.xml
├── python_parameters
│   └── __init__.py
├── resource
│   └── python_parameters
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

3 directories, 9 files

<span style="color:#888888">## 接着我们照例打开vsode来编辑工程</span>
$ code src/python_parameters
</code></span></span></span></span>

我们在python_parameters包中创建一个python_parameters_node.py源文件,内容如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">import</span> <span style="color:#f4bf75">rclpy</span>
<span style="color:#aa759f">import</span> <span style="color:#f4bf75">rclpy.node</span>
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">rcl_interfaces.msg</span> <span style="color:#aa759f">import</span> ParameterDescriptor

<span style="color:#aa759f">class</span> <span style="color:#f4bf75">MinimalParam</span><span style="color:#d0d0d0">(</span>rclpy<span style="color:#d0d0d0">.</span>node<span style="color:#d0d0d0">.</span>Node<span style="color:#d0d0d0">):</span>
    <span style="color:#aa759f">def</span> __init__<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        super<span style="color:#d0d0d0">().</span>__init__<span style="color:#d0d0d0">(</span><span style="color:#90a959">'minimal_param_node'</span><span style="color:#d0d0d0">)</span>

        my_parameter_descriptor <span style="color:#d0d0d0">=</span> ParameterDescriptor<span style="color:#d0d0d0">(</span>description<span style="color:#d0d0d0">=</span><span style="color:#90a959">'This parameter is mine!'</span><span style="color:#d0d0d0">)</span>

        self<span style="color:#d0d0d0">.</span>declare_parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">'my_parameter'</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">'world'</span><span style="color:#d0d0d0">,</span> my_parameter_descriptor<span style="color:#d0d0d0">)</span>

        self<span style="color:#d0d0d0">.</span>timer <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>create_timer<span style="color:#d0d0d0">(</span><span style="color:#90a959">1</span><span style="color:#d0d0d0">,</span> self<span style="color:#d0d0d0">.</span>timer_callback<span style="color:#d0d0d0">)</span>

    <span style="color:#aa759f">def</span> timer_callback<span style="color:#d0d0d0">(</span>self<span style="color:#d0d0d0">):</span>
        my_param <span style="color:#d0d0d0">=</span> self<span style="color:#d0d0d0">.</span>get_parameter<span style="color:#d0d0d0">(</span><span style="color:#90a959">'my_parameter'</span><span style="color:#d0d0d0">).</span>get_parameter_value<span style="color:#d0d0d0">().</span>string_value

        self<span style="color:#d0d0d0">.</span>get_logger<span style="color:#d0d0d0">().</span>info<span style="color:#d0d0d0">(</span><span style="color:#90a959">'Hello %s!'</span> <span style="color:#d0d0d0">%</span> my_param<span style="color:#d0d0d0">)</span>

        my_new_param <span style="color:#d0d0d0">=</span> rclpy<span style="color:#d0d0d0">.</span>parameter<span style="color:#d0d0d0">.</span>Parameter<span style="color:#d0d0d0">(</span>
            <span style="color:#90a959">'my_parameter'</span><span style="color:#d0d0d0">,</span>
            rclpy<span style="color:#d0d0d0">.</span>Parameter<span style="color:#d0d0d0">.</span>Type<span style="color:#d0d0d0">.</span>STRING<span style="color:#d0d0d0">,</span>
            <span style="color:#90a959">'world'</span>
        <span style="color:#d0d0d0">)</span>
        all_new_parameters <span style="color:#d0d0d0">=</span> <span style="color:#d0d0d0">[</span>my_new_param<span style="color:#d0d0d0">]</span>
        self<span style="color:#d0d0d0">.</span>set_parameters<span style="color:#d0d0d0">(</span>all_new_parameters<span style="color:#d0d0d0">)</span>

<span style="color:#aa759f">def</span> main<span style="color:#d0d0d0">():</span>
    rclpy<span style="color:#d0d0d0">.</span>init<span style="color:#d0d0d0">()</span>
    node <span style="color:#d0d0d0">=</span> MinimalParam<span style="color:#d0d0d0">()</span>
    rclpy<span style="color:#d0d0d0">.</span>spin<span style="color:#d0d0d0">(</span>node<span style="color:#d0d0d0">)</span>
    node<span style="color:#d0d0d0">.</span>destroy_node<span style="color:#d0d0d0">()</span>
    rclpy<span style="color:#d0d0d0">.</span>shutdown<span style="color:#d0d0d0">()</span>

<span style="color:#aa759f">if</span> __name__ <span style="color:#d0d0d0">==</span> <span style="color:#90a959">'__main__'</span><span style="color:#d0d0d0">:</span>
    main<span style="color:#d0d0d0">()</span>
</code></span></span></span></span>

在代码main函数中照例初始化创建一个MinimalParam类型的节点,然后循环执行节点,当从spin退出之后销毁节点并关闭ROS2。我们主要将一下MinimalParam。

MinimalParam的初始化(类似构造函数)函数中创建了一个名为minimal_param_node的节点并创建了一个ParameterDescriptor,然后使用declare_parameter创建一个key是my_parameter的参数,并为这个参数赋初值为“world”。然后创建一个定时器,每隔1s调用timer_callback函数。

在timer_callback函数中,先使用get_parameter获取参数值,并打印出来。然后我们使用set_parameters函数将参数值复原为“world”。

现在我们需要将这个文件添加进编译目标,让我们修改setup.py文件,修改entry_points为:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>entry_points<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">{</span>
    <span style="color:#90a959">'console_scripts'</span><span style="color:#d0d0d0">:</span> <span style="color:#d0d0d0">[</span>
        <span style="color:#90a959">'minimal_param_node = python_parameters.python_parameters_node:main'</span><span style="color:#d0d0d0">,</span>
    <span style="color:#d0d0d0">],</span>
<span style="color:#d0d0d0">},</span>
</code></span></span></span></span>

接着检查并编译:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src/python_parameters/ <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
All required rosdeps installed successfully
$ colcon build <span style="color:#f4bf75">--packages-select</span> python_parameters
Starting <span style="color:#d0d0d0">>>></span> python_parameters
<span style="color:#f4bf75">---</span> stderr: python_parameters                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> python_parameters <span style="color:#d0d0d0">[</span>0.90s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.50s]
  1 package had stderr output: python_parameters
</code></span></span></span></span>

编译成功后,我们可以运行这个节点:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 run python_parameters minimal_param_node
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706159642.638704504] <span style="color:#d0d0d0">[</span>minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706159643.631924780] <span style="color:#d0d0d0">[</span>minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706159644.631876017] <span style="color:#d0d0d0">[</span>minimal_param_node]: Hello world!

<span style="color:#888888"># ...</span>
</code></span></span></span></span>
2.11.2 在终端中修改参数

不要关闭正在运行minimal_param_node的终端,我们新打开一个终端。在新的终端中可以枚举当前可用的param:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 param list
/minimal_param_node:                                                  
  my_parameter                                                        
  use_sim_time    
</code></span></span></span></span>

也可以查看描述符:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 不妨先查看一下节点</span>
$ ros2 node list
/minimal_param_node
<span style="color:#888888">## 查看参数描述符</span>
$ ros2 param describe /minimal_param_node my_parameter
Parameter name: my_parameter
  Type: string
  Description: This parameter is mine!
  Constraints:
<span style="color:#888888">## 查看当前的参数设置</span>
$ ros2 param get /minimal_param_node my_parameter  
String value is: world
</code></span></span></span></span>

现在我们看一下当我们修改参数是,正在运行的节点输出信息有何变化

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 param set /minimal_param_node my_parameter moon
</code></span></span></span></span>

这时候我正在输出的信息由Hello World!变成了Hello Moon!

[INFO] [1706161965.181400961] [minimal_param_node]: Hello world!
[INFO] [1706161966.180624814] [minimal_param_node]: Hello moon!
[INFO] [1706161967.180764113] [minimal_param_node]: Hello world!
2.11.3 在launch file中修改参数

现在我们创建一个launch file文件放在package下的launch目录中,名称叫做python_parameters_launch.py,内容如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">from</span> <span style="color:#f4bf75">launch</span> <span style="color:#aa759f">import</span> LaunchDescription
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">launch_ros.actions</span> <span style="color:#aa759f">import</span> Node

<span style="color:#aa759f">def</span> generate_launch_description<span style="color:#d0d0d0">():</span>
    <span style="color:#aa759f">return</span> LaunchDescription<span style="color:#d0d0d0">([</span>
        Node<span style="color:#d0d0d0">(</span>
            package<span style="color:#d0d0d0">=</span><span style="color:#90a959">'python_parameters'</span><span style="color:#d0d0d0">,</span>
            executable<span style="color:#d0d0d0">=</span><span style="color:#90a959">'minimal_param_node'</span><span style="color:#d0d0d0">,</span>
            name<span style="color:#d0d0d0">=</span><span style="color:#90a959">'custom_minimal_param_node'</span><span style="color:#d0d0d0">,</span>
            output<span style="color:#d0d0d0">=</span><span style="color:#90a959">'screen'</span><span style="color:#d0d0d0">,</span>
            emulate_tty<span style="color:#d0d0d0">=</span>True<span style="color:#d0d0d0">,</span>
            parameters<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">[</span>
                <span style="color:#d0d0d0">{</span><span style="color:#90a959">'my_parameter'</span><span style="color:#d0d0d0">:</span> <span style="color:#90a959">'earth'</span><span style="color:#d0d0d0">}</span>
            <span style="color:#d0d0d0">]</span>
        <span style="color:#d0d0d0">)</span>
    <span style="color:#d0d0d0">])</span>
</code></span></span></span></span>

上面的代码和我们在2.10.3并无本质区别。

与2.10.3不同的是我们这次需要修改setup.py文件(而不是CMakeLists.txt),添加launch文件的路径:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#aa759f">import</span> <span style="color:#f4bf75">os</span>
<span style="color:#aa759f">from</span> <span style="color:#f4bf75">glob</span> <span style="color:#aa759f">import</span> glob
<span style="color:#888888"># ...
</span>
setup<span style="color:#d0d0d0">(</span>
  <span style="color:#888888"># ...
</span>  data_files<span style="color:#d0d0d0">=</span><span style="color:#d0d0d0">[</span>
      <span style="color:#888888"># ...
</span>      <span style="color:#d0d0d0">(</span>os<span style="color:#d0d0d0">.</span>path<span style="color:#d0d0d0">.</span>join<span style="color:#d0d0d0">(</span><span style="color:#90a959">'share'</span><span style="color:#d0d0d0">,</span> package_name<span style="color:#d0d0d0">),</span> glob<span style="color:#d0d0d0">(</span><span style="color:#90a959">'launch/*launch.[pxy][yma]*'</span><span style="color:#d0d0d0">)),</span>
    <span style="color:#d0d0d0">]</span>
  <span style="color:#d0d0d0">)</span>
</code></span></span></span></span>

上文只要是在data_files中添加一个条目,用来添加launch文件的路径。

然后来是编译测试:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep install <span style="color:#f4bf75">-i</span> <span style="color:#f4bf75">--from-path</span> src/python_parameters/ <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
<span style="color:#888888">#All required rosdeps installed successfully</span>
$ colcon build  <span style="color:#f4bf75">--packages-select</span> python_parameters 
Starting <span style="color:#d0d0d0">>>></span> python_parameters
<span style="color:#f4bf75">---</span> stderr: python_parameters                   
/home/galileo/.local/lib/python3.10/site-packages/setuptools/_distutils/cmd.py:66: SetuptoolsDeprecationWarning: setup.py install is deprecated.
<span style="color:#d0d0d0">!!</span>

        <span style="color:#aa759f">********************************************************************************</span>
        Please avoid running <span style="color:#90a959">``</span>setup.py<span style="color:#90a959">``</span> directly.
        Instead, use pypa/build, pypa/installer or other
        standards-based tools.

        See https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html <span style="color:#aa759f">for </span>details.
        <span style="color:#aa759f">********************************************************************************</span>

<span style="color:#d0d0d0">!!</span>
  self.initialize_options<span style="color:#d0d0d0">()</span>
<span style="color:#f4bf75">---</span>
Finished <span style="color:#d0d0d0"><<<</span> python_parameters <span style="color:#d0d0d0">[</span>0.82s]

Summary: 1 package finished <span style="color:#d0d0d0">[</span>1.43s]
  1 package had stderr output: python_parameters
</code></span></span></span></span>

在这个终端中source环境,然后运行launch一下刚才python_parameters_launch.py

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 launch python_parameters python_parameters_launch.py
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>launch]: All log files can be found below /home/galileo/.ros/log/2024-01-25-14-35-21-049774-Galileo-Dell-Linux-28706
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>launch]: Default logging verbosity is set to INFO
<span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>minimal_param_node-1]: process started with pid <span style="color:#d0d0d0">[</span>28707]
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706164522.213285310] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello earth!
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706164523.206513830] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706164524.206772311] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello world!
<span style="color:#d0d0d0">[</span>minimal_param_node-1] <span style="color:#d0d0d0">[</span>INFO] <span style="color:#d0d0d0">[</span>1706164525.206759469] <span style="color:#d0d0d0">[</span>custom_minimal_param_node]: Hello world!
</code></span></span></span></span>
2.11.4 总结

这样我们就完成了在C++和Python中使用参数的教程。我们可以看到,在C++和Python中使用参数,我们需要在节点初始化的时候声明参数,并在回调函数中获取参数值。在launch文件中修改参数,我们需要在launch文件中声明参数,并在节点初始化的时候设置参数。

另外有一点注意事项,在编写launch file的时候要确保参数名称和类型匹配,节点和可执行程序的名称要正确。否则可能会出错。

2.12 使用ros2doctor识别错误

本小节参照入门教程Using ros2doctor to identify issues的内容。

我们在笔记1中简单描述过ros2 doctor可以排查故障。这个命令的底层使用的工具就是ros2doctor.它可以检查ROS2的各个方面,包括平台、版本、网络、环境、运行系统等,并警告您可能的错误和问题原因。ros2doctorros2cli包的一部分。只要ros2cli被正确安装,ros2doctor就可以正常运行。

我使用ros2 doctor检查我的ros2结果如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 doctor
/opt/ros/humble/lib/python3.10/site-packages/ros2doctor/api/package.py: 112: UserWarning: joint_limits has been updated to a new version. local: 2.36.0 < latest: 2.37.0
/opt/ros/humble/lib/python3.10/site-packages/ros2doctor/api/package.py: 112: UserWarning: tricycle_steering_controller has been updated to a new version. local: 2.30.0 < latest: 2.32.0
<span style="color:#888888">## omit ...</span>
/opt/ros/humble/lib/python3.10/site-packages/ros2doctor/api/package.py: 119: UserWarning: Cannot find the latest versions of packages: python_parameters robotiq_hardware_tests robotiq_driver serial moveit2_tutorials <span style="color:#d0d0d0">[</span>...] Use <span style="color:#90a959">`</span>ros2 doctor <span style="color:#f4bf75">--report</span><span style="color:#90a959">`</span> to see full list.

All 5 checks passed
</code></span></span></span></span>

显示的这些警告提示我有一些packages需要更新。但是最终还是提示了All 5 checks passed。 如果因为某种原因导致我的包出错应该会有大概类似下面的提示:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>1/5 checks failed

Failed modules:  network
</code></span></span></span></span>

除了静态查找错误,ros2doctor还可以检查动态错误。比如按照入门教程的说法我先运行ros2 run turtlesim turtlesim_node然后运行ros2 run turtlesim turtle_teleop_key会显示额外的警告:

/opt/ros/humble/lib/python3.10/site-packages/ros2doctor/api/topic.py: 42: UserWarning: Publisher without subscriber detected on /turtle1/color_sensor.
/opt/ros/humble/lib/python3.10/site-packages/ros2doctor/api/topic.py: 42: UserWarning: Publisher without subscriber detected on /turtle1/pose.

这些警告提示/turtle1/color_sensor和/turtle1/pose没有订阅者。如果你设法订阅(可通过ros2 topic echo去订阅或者其它方法。),就会发现相应的警告消失了。

因为能够检测到动态(ROS2节点运行时)的错误,所以ros2doctor可以帮助我们更快的定位问题。

当需要更全面的排查警告或者错误的时候,一份完整的报告将是非常有用的。我们可以通过ros2 doctor --report命令生成报告。

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 doctor <span style="color:#f4bf75">--report</span>

   NETWORK CONFIGURATION
inet         : 127.0.0.1
<span style="color:#888888"># omit...</span>
OADCAST,MULTICAST,UP>
mtu          : 1500

   PACKAGE VERSIONS
robotiq_hardware_tests                             : latest<span style="color:#d0d0d0">=</span>, local<span style="color:#d0d0d0">=</span>0.0.1
<span style="color:#888888"># omit ...</span>
   PLATFORM INFORMATION
system           : Linux
<span style="color:#888888"># omit...</span>

   QOS COMPATIBILITY LIST
compatibility status    : No publisher/subscriber pairs found

   RMW MIDDLEWARE
middleware name    : rmw_fastrtps_cpp

   ROS 2 INFORMATION
distribution name      : humble
<span style="color:#888888"># omit...</span>

   TOPIC LIST
topic               : none
publisher count     : 0
subscriber count    : 0
</code></span></span></span></span>

生成的报告包含了NETWORK、PACKAGE VERSIONS、PLATFORM INFORMATION、QOS COMPATIBILITY LIST、RMW MIDDLEWARE、ROS 2 INFORMATION、TOPIC LIST等部分。

但是需要强调的是ros2doctor并不是一个调试工具,它不能帮助你排查你代码中的问题或者是系统部署情况的问题。

关于ros2doctor的更多信息,请参考ros2doctor文档。

2.13 创建和使用插件(C++)

本小节参照入门教程Creating and using plugins (C++)的内容。

2.13.1 插件库(Pluginlib)

插件(Plugin)是在现有软件基础上扩展功能的一种组件。插件是从运行时库(runtime。即共享对象、动态链接库)加载的动态可加载类。ROS2使用插件来扩展软件包的现有功能。插件对于扩展/修改应用程序行为非常有用,而无需应用程序源代码。

插件库(Pluginlib)是ROS包用来加载和卸载插件的C++库。 使用pluginlib,您不必将应用程序显式链接到包含类的库;相反pluginlib可以在任何时候打开包含导出类的库,而应用程序无需事先了解该库或包含类定义的头文件。

2.13.2 创建基础包(base package)

本教程将创建两个packages。一个是基础包(base package),用来定义基类(Base Class);另一个是插件包(plugin package),用来实现具体的插件(Plugin)。在基类中我们将创建一个通用的多边形类,并在插件包中实现具体的多边形类。我们不妨将两个包放在一个工作区中,命名为demo9_ws.

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#888888">## 先确保已经cd到你准备存放工作区的目录</span>
mkdir <span style="color:#f4bf75">-p</span> demo9_ws/src
cd demo9_ws
</code></span></span></span></span>

这一部分我们先来创建基础包。基础包的名字叫做polygon_base,依赖于pluginlib库,内部节点的名字我们显式命名为area_node。现在让我们创建这个ament_cmake包吧:

$ ros2 pkg create polygon_base --destination-directory src --build-type ament_cmake --license Apache-2.0 --dependencies pluginlib --node-name area_node --description "A package for create regular polygons"
$ tree src/polygon_base/
src/polygon_base/
├── CMakeLists.txt
├── include
│   └── polygon_base
├── LICENSE
├── package.xml
└── src
    └── area_node.cpp

3 directories, 4 files

## 现在打开vscode
$ code src/polygon_base/

我们创建一个头文件(位于include/polygon_base/目录下)regular_polygon.hpp,内容如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#ifndef POLYGON_BASE_REGULAR_POLYGON_HPP
#define POLYGON_BASE_REGULAR_POLYGON_HPP
</span>
<span style="color:#aa759f">namespace</span> polygon_base
<span style="color:#d0d0d0">{</span>
  <span style="color:#aa759f">class</span> <span style="color:#f4bf75">RegularPolygon</span>
  <span style="color:#d0d0d0">{</span>
    public:
      <span style="color:#aa759f">virtual</span> <span style="color:#d28445">void</span> initialize<span style="color:#d0d0d0">(</span><span style="color:#d28445">double</span> side_length<span style="color:#d0d0d0">)</span> <span style="color:#d0d0d0">=</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
      <span style="color:#aa759f">virtual</span> <span style="color:#d28445">double</span> area<span style="color:#d0d0d0">()</span> <span style="color:#d0d0d0">=</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
      <span style="color:#aa759f">virtual</span> <span style="color:#d0d0d0">~</span>RegularPolygon<span style="color:#d0d0d0">(){}</span>

    protected:
      RegularPolygon<span style="color:#d0d0d0">(){}</span>
  <span style="color:#d0d0d0">};</span>
<span style="color:#d0d0d0">}</span>  <span style="color:#888888">// namespace polygon_base</span>

<span style="color:#f4bf75">#endif  // POLYGON_BASE_REGULAR_POLYGON_HPP
</span></code></span></span></span></span>

这个类定义了一个基类RegularPolygon,它有三个虚函数:initialize、area和析构函数。initialize函数用来设置多边形的边长,area函数用来计算多边形的面积。 需要注意的一件事是初始化方法的存在。 对于pluginlib,需要一个不带参数的构造函数,因此如果类需要任何参数,我们使用initialize方法将它们传递给对象。

尽管我们定义了类,但是我们稍后再来实现他。现在我们需要让编译器能够识别这个文件。先修改CMakelists.txt文件,添加如下内容:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>install<span style="color:#d0d0d0">(</span>
  DIRECTORY include/
  DESTINATION include
<span style="color:#d0d0d0">)</span>
</code></span></span></span></span>

请注意我们在创建包的时候指明了node name,所以工程src下还会自动生成一个area_node.cpp的文件。而且CMakefiles.txt文件也会自动包含了相关cpp文件。

2.13.3 创建插件包(plugin package)

现在我们创建一个名字叫polygon_plugins的包,依赖于pluginlib和刚刚创建的package:polygon_base。此外我们还需要制定她的--library-namepolygon_plugins--plugin-namepolygon_plugin--description为”A package for create regular polygons”。现在我们来创建它吧:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 pkg create polygon_plugins <span style="color:#f4bf75">--destination-directory</span> src <span style="color:#f4bf75">--build-type</span> ament_cmake <span style="color:#f4bf75">--license</span> Apache-2.0 <span style="color:#f4bf75">--dependencies</span> polygon_base <span style="color:#f4bf75">--library-name</span> polygon_plugins <span style="color:#f4bf75">--dependencies</span> polygon_base pluginlib  <span style="color:#f4bf75">--description</span> <span style="color:#90a959">"A package for create regular polygons"</span>
$ tree src/polygon_plugins/
src/polygon_plugins/
├── CMakeLists.txt
├── include
│   └── polygon_plugins
│       ├── polygon_plugins.hpp
│       └── visibility_control.h
├── LICENSE
├── package.xml
└── src
    └── polygon_plugins.cpp

3 directories, 6 files
<span style="color:#888888">## 现在打开vscode</span>
$ code src/polygon_plugins/
</code></span></span></span></span>

然后我们添加在polygon_plugins.cpp文件添加如下代码:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include "polygon_plugins/polygon_plugins.hpp"
#include <polygon_base/regular_polygon.hpp>
#include <cmath>
</span>
<span style="color:#aa759f">namespace</span> polygon_plugins
<span style="color:#d0d0d0">{</span>
  <span style="color:#aa759f">class</span> <span style="color:#f4bf75">Square</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon
  <span style="color:#d0d0d0">{</span>
    public:
      <span style="color:#d28445">void</span> initialize<span style="color:#d0d0d0">(</span><span style="color:#d28445">double</span> side_length<span style="color:#d0d0d0">)</span> <span style="color:#aa759f">override</span>
      <span style="color:#d0d0d0">{</span>
        side_length_ <span style="color:#d0d0d0">=</span> side_length<span style="color:#d0d0d0">;</span>
      <span style="color:#d0d0d0">}</span>

      <span style="color:#d28445">double</span> area<span style="color:#d0d0d0">()</span> <span style="color:#aa759f">override</span>
      <span style="color:#d0d0d0">{</span>
        <span style="color:#aa759f">return</span> side_length_ <span style="color:#d0d0d0">*</span> side_length_<span style="color:#d0d0d0">;</span>
      <span style="color:#d0d0d0">}</span>

    protected:
      <span style="color:#d28445">double</span> side_length_<span style="color:#d0d0d0">;</span>
  <span style="color:#d0d0d0">};</span>

  <span style="color:#aa759f">class</span> <span style="color:#f4bf75">Triangle</span> <span style="color:#d0d0d0">:</span> <span style="color:#aa759f">public</span> polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon
  <span style="color:#d0d0d0">{</span>
    public:
      <span style="color:#d28445">void</span> initialize<span style="color:#d0d0d0">(</span><span style="color:#d28445">double</span> side_length<span style="color:#d0d0d0">)</span> <span style="color:#aa759f">override</span>
      <span style="color:#d0d0d0">{</span>
        side_length_ <span style="color:#d0d0d0">=</span> side_length<span style="color:#d0d0d0">;</span>
      <span style="color:#d0d0d0">}</span>

      <span style="color:#d28445">double</span> area<span style="color:#d0d0d0">()</span> <span style="color:#aa759f">override</span>
      <span style="color:#d0d0d0">{</span>
        <span style="color:#aa759f">return</span> <span style="color:#90a959">0.5</span> <span style="color:#d0d0d0">*</span> side_length_ <span style="color:#d0d0d0">*</span> getHeight<span style="color:#d0d0d0">();</span>
      <span style="color:#d0d0d0">}</span>

      <span style="color:#d28445">double</span> getHeight<span style="color:#d0d0d0">()</span>
      <span style="color:#d0d0d0">{</span>
        <span style="color:#aa759f">return</span> sqrt<span style="color:#d0d0d0">((</span>side_length_ <span style="color:#d0d0d0">*</span> side_length_<span style="color:#d0d0d0">)</span> <span style="color:#d0d0d0">-</span> <span style="color:#d0d0d0">((</span>side_length_ <span style="color:#d0d0d0">/</span> <span style="color:#90a959">2</span><span style="color:#d0d0d0">)</span> <span style="color:#d0d0d0">*</span> <span style="color:#d0d0d0">(</span>side_length_ <span style="color:#d0d0d0">/</span> <span style="color:#90a959">2</span><span style="color:#d0d0d0">)));</span>
      <span style="color:#d0d0d0">}</span>

    protected:
      <span style="color:#d28445">double</span> side_length_<span style="color:#d0d0d0">;</span>
  <span style="color:#d0d0d0">};</span>
<span style="color:#d0d0d0">}</span>

<span style="color:#f4bf75">#include <pluginlib/class_list_macros.hpp>
</span>
PLUGINLIB_EXPORT_CLASS<span style="color:#d0d0d0">(</span>polygon_plugins<span style="color:#d0d0d0">::</span>Square<span style="color:#d0d0d0">,</span> polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon<span style="color:#d0d0d0">)</span>
PLUGINLIB_EXPORT_CLASS<span style="color:#d0d0d0">(</span>polygon_plugins<span style="color:#d0d0d0">::</span>Triangle<span style="color:#d0d0d0">,</span> polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon<span style="color:#d0d0d0">)</span>
</code></span></span></span></span>

上面分别定义了两个类:一个创建正三角形,一个创建正方形。并在area区域求边长。代码的最后两行使用PLUGINLIB_EXPORT_CLASS宏来导出这两个类。

上述步骤允许在加载包含的库时创建插件实例,但插件加载器仍然需要一种方法来查找该库并知道在该库中引用什么。为此,我们还将创建一个XML文件,该文件以及包清单中的特殊导出行,使有关我们的插件的所有必要信息可供ROS工具链使用。我们需要在package根目录下创建一个叫做plugins的xml文件。并填充如下内容:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><library path="polygon_plugins">
  <class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon">
    <description>This is a square plugin.</description>
  </class>
  <class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon">
    <description>This is a triangle plugin.</description>
  </class>
</library>
</code></span></span></span></span>

在这个文件中描述了插件类和基类的名称,并添加了必要的描述。有几点需要注意:

  • library标签提供了包含我们要导出的插件的库的相对路径。在 ROS2中,这只是库的名称。在ROS1中,它包含前缀lib或有时lib/lib(即lib/libpolygon_plugins),但这里更简单。
  • class标签声明了我们要从库中导出的插件。 我们来看看它的参数:
    • type:插件的完全限定类型。 对于我们来说,这就是 Polygon_plugins::Square。
    • base_class:插件的完全限定基类类型。 对于我们来说,那就是Polygon_base::RegularPolygon。
    • description:插件及其用途的描述。

我们还需要让编译器输出插件,需要在CMakeLists.txt文件中添加:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>pluginlib_export_plugin_description_file<span style="color:#d0d0d0">(</span>polygon_base plugins.xml<span style="color:#d0d0d0">)</span>
</code></span></span></span></span>

需要注意的是这和在ROS1中的是有区别的。在ROS1中需要修改package.xml文件,添加:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75"><export></span>
  <span style="color:#f4bf75"><plugin</span> <span style="color:#6a9fb5">plugin=</span><span style="color:#90a959">"${prefix}/polygon_plugins"</span><span style="color:#f4bf75">></span>
    <span style="color:#f4bf75"><class</span> <span style="color:#6a9fb5">name=</span><span style="color:#90a959">"polygon_plugins::Square"</span> <span style="color:#f4bf75">/></span>
    <span style="color:#f4bf75"><class</span> <span style="color:#6a9fb5">name=</span><span style="color:#90a959">"polygon_plugins::Triangle"</span> <span style="color:#f4bf75">/></span>
  <span style="color:#f4bf75"></plugin></span>
<span style="color:#f4bf75"></export></span>
</code></span></span></span></span>

pluginlib_export_plugin_description_file 命令的参数是:

  • 带有基类的包,即polygon_base。
  • 插件声明xml的相对路径,即plugins.xml。
2.13.4 修改基础包(base package)以使用插件

我们现在修改plugin_base包中的area_node.cpp文件,使其使用插件。首先,我们需要包含插件的头文件:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include <polygon_plugins/polygon_plugins.hpp>
</span></code></span></span></span></span>

然后,我们需要创建一个插件管理器:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code><span style="color:#f4bf75">#include <pluginlib/class_loader.hpp>
</span>
<span style="color:#d28445">int</span> main<span style="color:#d0d0d0">(</span><span style="color:#d28445">int</span> argc<span style="color:#d0d0d0">,</span> <span style="color:#d28445">char</span><span style="color:#d0d0d0">**</span> argv<span style="color:#d0d0d0">)</span>
<span style="color:#d0d0d0">{</span>
  <span style="color:#888888">// To avoid unused parameter warnings</span>
  <span style="color:#d0d0d0">(</span><span style="color:#d28445">void</span><span style="color:#d0d0d0">)</span> argc<span style="color:#d0d0d0">;</span>
  <span style="color:#d0d0d0">(</span><span style="color:#d28445">void</span><span style="color:#d0d0d0">)</span> argv<span style="color:#d0d0d0">;</span>

  pluginlib<span style="color:#d0d0d0">::</span>ClassLoader<span style="color:#d0d0d0"><</span>polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon<span style="color:#d0d0d0">></span> poly_loader<span style="color:#d0d0d0">(</span><span style="color:#90a959">"polygon_base"</span><span style="color:#d0d0d0">,</span> <span style="color:#90a959">"polygon_base::RegularPolygon"</span><span style="color:#d0d0d0">);</span>

  <span style="color:#aa759f">try</span>
  <span style="color:#d0d0d0">{</span>
    std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon<span style="color:#d0d0d0">></span> triangle <span style="color:#d0d0d0">=</span> poly_loader<span style="color:#d0d0d0">.</span>createSharedInstance<span style="color:#d0d0d0">(</span><span style="color:#90a959">"polygon_plugins::Triangle"</span><span style="color:#d0d0d0">);</span>
    triangle<span style="color:#d0d0d0">-></span>initialize<span style="color:#d0d0d0">(</span><span style="color:#90a959">10.0</span><span style="color:#d0d0d0">);</span>

    std<span style="color:#d0d0d0">::</span>shared_ptr<span style="color:#d0d0d0"><</span>polygon_base<span style="color:#d0d0d0">::</span>RegularPolygon<span style="color:#d0d0d0">></span> square <span style="color:#d0d0d0">=</span> poly_loader<span style="color:#d0d0d0">.</span>createSharedInstance<span style="color:#d0d0d0">(</span><span style="color:#90a959">"polygon_plugins::Square"</span><span style="color:#d0d0d0">);</span>
    square<span style="color:#d0d0d0">-></span>initialize<span style="color:#d0d0d0">(</span><span style="color:#90a959">10.0</span><span style="color:#d0d0d0">);</span>

    printf<span style="color:#d0d0d0">(</span><span style="color:#90a959">"Triangle area: %.2f</span><span style="color:#8f5536">\n</span><span style="color:#90a959">"</span><span style="color:#d0d0d0">,</span> triangle<span style="color:#d0d0d0">-></span>area<span style="color:#d0d0d0">());</span>
    printf<span style="color:#d0d0d0">(</span><span style="color:#90a959">"Square area: %.2f</span><span style="color:#8f5536">\n</span><span style="color:#90a959">"</span><span style="color:#d0d0d0">,</span> square<span style="color:#d0d0d0">-></span>area<span style="color:#d0d0d0">());</span>
  <span style="color:#d0d0d0">}</span>
  <span style="color:#aa759f">catch</span><span style="color:#d0d0d0">(</span>pluginlib<span style="color:#d0d0d0">::</span>PluginlibException<span style="color:#d0d0d0">&</span> ex<span style="color:#d0d0d0">)</span>
  <span style="color:#d0d0d0">{</span>
    printf<span style="color:#d0d0d0">(</span><span style="color:#90a959">"The plugin failed to load for some reason. Error: %s</span><span style="color:#8f5536">\n</span><span style="color:#90a959">"</span><span style="color:#d0d0d0">,</span> ex<span style="color:#d0d0d0">.</span>what<span style="color:#d0d0d0">());</span>
  <span style="color:#d0d0d0">}</span>

  <span style="color:#aa759f">return</span> <span style="color:#90a959">0</span><span style="color:#d0d0d0">;</span>
<span style="color:#d0d0d0">}</span>
</code></span></span></span></span>

在程序开头先用pluginlib::ClassLoader类来加载插件。这个类需要两个必传参数和两个默认参数:

  • 第一个参数package是插件所在的包的名称。
  • 第二个参数base_class是插件的基类名称。
  • 第三个参数attrib_name是要在 maniext.xml 文件中搜索的属性,默认为“plugin”。
  • 第四个参数plugin_xml_paths是搜索plugin.xml文件的路径列表,默认通过ros::package::getPlugins()爬取。

前两个参数必须指明。在上面的代码中插件所在的包是我们创建的基包(Base Package)”polygon_base”;插件的基类是我们在基包中创建的基类(Base Class)”polygon_base::RegularPolygon”。

因为createSharedInstance方法的加载可能会有异常,所以我们用try-catch块来捕获异常。在try块中,我们使用createSharedInstance方法创建了两个插件实例,分别是正三角形和正方形。我们调用initialize方法来设置边长,并调用area方法来计算面积。

查看createSharedInstance的源码可以看到,它会调用loadLibraryForClass方法来加载库,然后调用createUniqueInstance方法来创建实例。

重要提示:定义此节点的Polygon_base包不依赖于Polygon_plugins类。插件将动态加载,无需声明任何依赖项。 此外,我们使用硬编码的插件名称实例化类,但您也可以使用参数等动态地执行此操作。

但是这个例子其实并没有很好的展示插件如何动态加载的。我们需要在运行时加载插件,而不仅仅是在编译时。好的软件应该提供一个机制来动态加载插件,而不仅仅是编译时。

2.13.5 编译和运行

现在我们可以编译和运行程序了。首先,我们需要编译polygon_base包:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ rosdep check <span style="color:#f4bf75">--from-paths</span> src <span style="color:#f4bf75">--ignore-src</span> <span style="color:#f4bf75">--rosdistro</span> humble <span style="color:#f4bf75">-y</span>
All system dependencies have been satisfied
$ colcon build <span style="color:#f4bf75">--packages-select</span> polygon_base
Starting <span style="color:#d0d0d0">>>></span> polygon_base
Finished <span style="color:#d0d0d0"><<<</span> polygon_base <span style="color:#d0d0d0">[</span>2.78s]                     

Summary: 1 package finished <span style="color:#d0d0d0">[</span>3.41s]
</code></span></span></span></span>

可以看到在polygon_plugins尚未编译的时候,polygon_base已经成功编译。也说明了基包并不依赖于插件本身。然后,我们可以编译polygon_plugins包:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ colcon build <span style="color:#f4bf75">--packages-select</span> polygon_plugins
</code></span></span></span></span>

现在我们可以验证一下包ready:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ source install/setup.bash
$ ros2 pkg list | grep polygon
polygon_base                    
polygon_plugins                 
$ ros2 pkg executables polygon_base
polygon_base area_node
</code></span></span></span></span>

最后,我们可以运行area_node节点:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>$ ros2 run polygon_base area_node
</code></span></span></span></span>

输出应该如下:

<span style="color:#2b2b2b"><span style="color:#d0d0d0"><span style="background-color:#333333"><span style="color:#d0d0d0"><code>Triangle area: 43.30
Square area: 100.00
</code></span></span></span></span>

area_node节点成功运行,并自动完成插件的加载。

2.13.6 总结

至此,我们完成了插件的创建和使用。插件的功能目前我还没有深刻体会,后面再继续了解吧。

<原文第三章持续编辑中>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/366231.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring框架——主流框架

文章目录 Spring(轻量级容器框架)Spring 学习的核心内容-一图胜千言IOC 控制反转 的开发模式Spring快速入门Spring容器剖析手动开发- 简单的 Spring 基于 XML 配置的程序课堂练习 Spring 管理 Bean-IOCSpring 配置/管理 bean 介绍Bean 管理包括两方面: Bean 配置方式基于 xml 文…

2023年上-未来几年我要做什么

1月份&#xff0c;离职。 2月份&#xff0c;春节休假回来&#xff0c;中旬去参加了一个月的瑜伽培训&#xff0c;学会了倒立、鹤蝉。。。。 3月份&#xff0c;瑜伽培训结束&#xff0c;开始收拾房子&#xff0c;并调研各类项目。 4月份&#xff0c;参与了朋友的区块链项目 …

echarts step line

https://ppchart.com/#/ <template><div class"c-box" ref"jsEchart"></div> </template><script> import * as $echarts from echarts // 事件处理函数 export default {props: {// 需要传递的数据data: {type: Array,defa…

Day06-Linux下目录命令讲解及重要文件讲解

Day06-Linux下目录命令讲解及重要文件讲解 1. Linux目录文件1.1 Linux系统目录结构介绍1.1.1 Linux与Windows目录结构对比 1.2 重要的Linux配置文件介绍1.2.1 /etc系统初始化及设置相关重要文件1.2.2 /usr目录的重要知识介绍------应用程序目录1.2.3 /var目录下的路径知识-----…

thinkphp项目之composer快速安装使用

引言 由于项目的需求&#xff0c;thinkphp项目使用到composer。网上搜索有一堆的教程使用&#xff0c;根据自己的需要摸索了下。 步骤 1. 安装phpstudy v8&#xff0c;这个经常用的运行环境&#xff0c;方便好多开发者。安装教程一步一步到最后就行。 2. 安装composer组件&a…

Github 2F2【解决】经验帖-PPHub登入

最近在做项目时,Github总是出问题,这是一经验贴 Github 2F2登入问题【无法登入】PPhub 2F2是为了安全,更好的生态 启用 2FA 二十八 (28) 天后,要在使用 GitHub.com 时 2FA 检查 物理安全密钥、Windows Hello 或面容 ID/触控 ID、SMS、GitHub Mobile 都可以作为 2F2 的工…

CTF特训(二):青少年CTF-MISC部分WP

FLAG&#xff1a;当觉得自己很菜的时候&#xff0c;就静下心来学习 专研方向:MISC&#xff0c;CTF 每日emo&#xff1a;听一千遍反方向的钟&#xff0c;我们能回到过去吗&#xff1f; CTF特训(二)&#xff1a;青少年CTF-MISC部分WP&#xff1a; 文章目录 CTF特训(二)&#xff1…

LeetCode:283. 移动零

283. 移动零 1&#xff09;题目2&#xff09;代码方法一&#xff1a;两层for循环方法二&#xff1a;使用双指针 3&#xff09;结果方法一结果方法二结果 1&#xff09;题目 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的…

计算机毕业设计 | SpringBoot 房屋租赁网 房屋租赁平台(附源码)

1&#xff0c;绪论 1.1 背景调研 在房地产行业持续火热的当今环境下&#xff0c;房地产行业和互联网行业协同发展&#xff0c;互相促进融合已经成为一种趋势和潮流。本项目实现了在线房产平台的功能&#xff0c;多种技术的灵活运用使得项目具备很好的用户体验感。 这个项目的…

day24打卡

day24打卡 思路&#xff1a;画出决策树&#xff0c;暴力枚举。子集问题 决策树&#xff1a; 函数头&#xff1a;void dfs(int n, int k, int pos) 函数体&#xff1a; ​ 出口&#xff1a;全局变量count k 保存结果到全局变量ret中 ​ 子问题&#xff1a;从pos位置向后变…

Linux服务详解

如有错误或有补充&#xff0c;以及任何改进的意见&#xff0c;请在评论区留下您的高见&#xff0c;同时文中给出大部分命令的示例&#xff0c;即是您暂时无法在Linux中查看&#xff0c;您也可以知道各种操作的功能以及输出 如果觉得本文写的不错&#xff0c;不妨点个赞&#x…

C++ //练习 3.36 编写一段程序,比较两个数组是否相等。再写一段程序,比较两个vector对象是否相等。

C Primer&#xff08;第5版&#xff09; 练习 3.36 练习 3.36 编写一段程序&#xff0c;比较两个数组是否相等。再写一段程序&#xff0c;比较两个vector对象是否相等。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块 /******…

WordPress可以做企业官网吗?如何用wordpress建公司网站?

我们在国内看到很多个人博客网站都是使用WordPress搭建&#xff0c;但是企业官网的相对少一些&#xff0c;那么WordPress可以做企业官网吗&#xff1f;如何用wordpress建公司网站呢&#xff1f;下面boke112百科就跟大家简单说一下。 WordPress是一款免费开源的内容管理系统&am…

主机安全加固之-openssh版本升级

升级openssh之前&#xff0c;为了保证能正常通过工具连接主机&#xff0c;咱们开启telnet服务&#xff0c;通过telnet的方式登录主机 一&#xff1a;开启telnet服务 1.安装telnet服务 [rootlocalhost ~]# yum install –y telnet telnet-server xinetd2.修改telnet服务配置文…

Multisim14.0仿真(四十一)交通信号灯仿真设计

一、功能简介&#xff1a; 1&#xff09;、采用两片74LS192做减法计数器&#xff0c;实现倒计时功能。 2&#xff09;、采用DCD数码管显示时间。 3&#xff09;、采用4个TRAFFIC_LIGHT_SINGLE红绿灯 4&#xff09;、采用74LS160和74LS138实现对红绿灯的逻辑控制。 5&#xff09…

【cmu15445c++入门】(5)C++ 包装类(管理资源的类)

一、背景 c包装类 二、运行代码 // A C wrapper class is a class that manages a resource. A resource // could be memory, file sockets, or a network connection. Wrapper classes // often use the RAII (Resource Acquisition is Initialization) C // programming…

printf死翘翘

本来想把我的单片机玩一下&#xff0c;寄给在大学搞研究的一个朋友&#xff0c;但竟然挂在printf里面&#xff0c;大概知道是什么位置出问题&#xff0c;但是还想不清楚什么原因。 我先是在stc51单片机里面搞了串口&#xff0c;然后我想用串口重定向到printf做调试&#xff0c;…

深度学习实战70-数学教材智能问答MathGPT模型与题目latex的pdf生成技术

大家好,我是微学AI ,今天给大家介绍一下深度学习实战70-数学教材智能问答MathGPT模型与题目latex的pdf生成技术,本文利用MathGPT数学大模型实现的数学教材智能问答系统。该系统结合了自然语言处理和数学知识图谱,能够理解用户的数学问题,并提供准确的答案和解析,随时随地请…

Linux系统中的容器化技术

当谈到容器化技术时&#xff0c;Docker和Kubernetes是两个最为知名和广泛使用的工具。它们在Linux系统中发挥着重要的作用&#xff0c;为应用程序的部署、管理和扩展提供了强大的工具和框架。 首先&#xff0c;让我们来了解一下什么是容器化技术。容器化技术是一种虚拟化技术&a…

网络原理-TCP/IP(4)

TCP原理 滑动窗口 之前我们讲过了确认应答策略,对发送的每一个数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段. 确认应答,超时重传,连接管理这样的特性都是为了保证可靠运输,但就是付出了传输效率(单位时间能传输数据的多少)的代价,因为确认应答机制导致了时间大…