ROS 2机器人开发--第一个节点
目录
1 编写第一个节点
2 使用功能包组织C++节点
3 功能包结构分析
4 C++面向对象编程
5 C++新特性 (必看)
5.1 自动类型推导
5.2 智能指针
5.3 Lambda表达式
今天,我们将一起探索如何编写ROS 2程序,以及如何将代码编译成可执行文件。
1 编写第一个节点
首先在主目录下创建chapt2/ 文件夹 , 并用VS Code打开该文件夹
新建ros2_cpp_node.cpp文件,在文件中编写如图所示的代码。
新建CMakeLists.txt文件, 在chapt2/下编写同名文件,内容如图所示。
然后继续输入cmake . make编译。
发现报错!
这是因为代码中引用了rclcpp.hpp
头文件,但该文件并不位于系统的默认头文件路径中,而是存储在ROS 2的安装目录下。为了使ros2_cpp_node
能够正确找到并依赖该头文件,可以通过CMake指令在chapt2/CMakeLists.txt
文件末尾添加相应的代码,如图所示。
# 指定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.8)
# 定义项目的名称
project(ros2_cpp)
# 添加一个可执行文件
# 第一个参数是目标名称(可执行文件名),第二个参数是源文件
add_executable(ros2_cpp_node ros2_cpp_node.cpp)
# 查找必要的 ROS 2 组件(rclcpp 是 ROS 2 的 C++ 客户端库)
# REQUIRED 表示如果找不到该组件,CMake 将报错并停止
find_package(rclcpp REQUIRED)
# 将 rclcpp 的头文件目录添加到目标的包含目录中
# PUBLIC 表示这些头文件目录也会被链接到目标的依赖项中
target_include_directories(ros2_cpp_node PUBLIC ${rclcpp_INCLUDE_DIRS})
# 将 rclcpp 的库链接到目标可执行文件中
target_link_libraries(ros2_cpp_node ${rclcpp_LIBRARIES})
这三行代码分别完成了依赖库的查找、头文件路径的添加以及动态库的链接。其中,find_package
指令能够在更广泛的目录中搜索依赖项。在找到rclcpp
的头文件和库文件后,它还会递归地查找rclcpp
所依赖的其他组件。
添加后再次执行make 操作,就可以看到可执行文件 ros2_cpp_node 已经生成,命令及结果如图所示。
输入./ros2_cpp_node运行,命令及结果如代码如图所示。
2 使用功能包组织C++节点
一个完整的机器人系统通常由多个功能模块构成,因此需要将多个功能包进行整合。为此,ROS 2开发者引入了“工作空间(Workspace)”的概念。使用VS Code打开chapt4/
目录,通过集成终端进入chapt4
目录,并输入以下代码。
mkdir -p ws00_helloworld/src #创建工作空间以及子级目录 src,工作空间名称可以自定义
cd ws00_helloworld #进入工作空间
colcon build #编译
在上述命令中,-p
参数用于递归创建目录。在创建ws00_helloworld
目录后,还会在其下创建一个src
文件夹,从而形成一个工作空间。你可能会疑惑,这不就是一个普通的文件夹吗?为何能称作工作空间?实际上,工作空间是一种概念和约定。在开发过程中,所有功能包都会被放置在src
目录下,并在与src
同级的目录中运行colcon
进行构建。此时,生成的build
、install
和log
等目录会与src
保持同级。
上述指令执行完毕,将创建ws00_helloworld目录,且该目录下包含build、install、log、src共四个子级目录。
先进入src,再输入如下所示的命令:
ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld
ros2 pkg create
是用于创建功能包的指令,其中 pkg01_helloworld_cpp
是功能包的名称,而 --build-type ament_cmake
参数用于指定功能包的构建类型为 ament_cmake
。从日志信息可以看出,该命令在当前目录下生成了一个名为 pkg01_helloworld_cpp
的文件夹,并在其中创建了一些默认的文件和子文件夹。在 VS Code 的左侧资源管理器中展开该文件夹,其目录结构如图所示。
进入pkg01_helloworld_cpp/src目录,该目录下有一helloworld.cpp文件,修改文件内容如下:
#include "rclcpp/rclcpp.hpp"
int main(int argc, char ** argv)
{
// 初始化 ROS2
rclcpp::init(argc,argv);
// 创建节点
auto node = rclcpp::Node::make_shared("helloworld_node");
// 输出文本
RCLCPP_INFO(node->get_logger(),"hello world!");
// 释放资源
rclcpp::shutdown();
return 0;
}
在步骤1中创建功能包时,所使用的命令已经自动生成并配置了必要的配置文件。然而,在实际应用中,通常需要手动编辑这些配置文件以满足特定需求。因此,这里对相关内容进行简要介绍。主要涉及的配置文件有两个,分别是功能包目录下的package.xml
和CMakeLists.txt
。
package.xml文件内容如下:
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>pkg01_helloworld_cpp</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="ubuntu64@todo.todo">ros2</maintainer>
<license>TODO: License declaration</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- 所需要依赖 -->
<depend>rclcpp</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
CMakeLists.txt文件内容如下:
cmake_minimum_required(VERSION 3.8)
project(pkg01_helloworld_cpp)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
# 引入外部依赖包
find_package(rclcpp REQUIRED)
# 映射源文件与可执行文件
add_executable(helloworld src/helloworld.cpp)
# 设置目标依赖库
ament_target_dependencies(
helloworld
"rclcpp"
)
# 定义安装规则
install(TARGETS helloworld
DESTINATION lib/${PROJECT_NAME})
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
编译,终端下进入到工作空间,执行如下指令:
colcon build
执行,终端下进入到工作空间,执行如下指令:
source install/setup.bash
ros2 run pkg01_helloworld_cpp helloworld
成功执行!!!
3 功能包结构分析
在 VS Code中的资源管理器中打开文件夹,并将其子文件夹完全展开,
可以看到如图所示的目录结构。
该目录非常简洁,包含2个文件夹、3个文件,下面将逐 一介绍。
● include
该目录用于存放C++ 的头文件,如果要编写头文件, 一般 都放置在这个目录下。
● src
代码资源目录,可以放置节点或其他相关代码。
● CMakeLists.txt
该文件是C/C++ 构建系统CMake 的配置文件,在该文件中添加指令,即可完成依赖查 找、可执行文件添加、安装等工作。
● LICENSE
该文件是功能包的许可证。创建该功能包时使用了--license Apache-2.0参数,这个文件内容就是Apache-2.0 的协议内容,在2.2.2节中有关于这个协议的简单介绍。
● package.xml
该文件是功能包的清单文件,每个ROS 2的功能包都会包含这个文件,和2.2.2节中 Python 功能包中的package.xml 功能相同。它的更多用法会在下一小节进行讲解。
当然,除了上面这些文件和文件夹,在实际开发中还可以添加其他目录和文件,比如用于放置地图的map目录、用于放置参数的config 目录等。
4 C++面向对象编程
C++是一门面向对象的语言,接下来通过一个例子去学习他。
#include<string>
#include"rclcpp/rclcpp.hpp"
class PersonNode :public rclcpp::Node
{
private:
std::string name_;
int age_;
public:
PersonNode(const std::string &node_name, const std::string &name,
const int &age) : Node(node_name)
{
this->name_ =name;
this->age_ =age;
};
void eat(const std::string &food_name)
{
RCLCPP_INEO(this->get_logger(),"我是%s,今年%d岁,我现在正在%s",
name_.c_str(),age_,food_name.c_str());
};
};
int main(int argc,char **argv)
{
rclcpp::init(argc,argv);
auto node = td::make_shared<PersonNode>("cpp_node","法外狂徒张三",18); node->eat("北京烤鸭");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
从上至下分析代码,首先引入了string
和rclcpp/rclcpp.hpp
两个头文件。其中,包含string
的原因是节点名称和姓名需要用字符串形式表示。
接着,通过class
关键字定义了PersonNode
类,并使其继承自rclcpp::Node
。在类的内部,定义了private
部分,包含姓名和年龄两个属性。
在public
部分,定义了PersonNode
的构造函数,并传入节点名称、姓名和年龄作为参数。需要注意的是,参数传递采用的是引用方式,在std::string
后添加&
表示传递引用。引用类似于指针,但避免了不必要的数据复制,从而提高代码效率。std::string
前的const
修饰符限制变量为只读,防止其在方法内被意外修改,增强代码的安全性。构造函数后通过Node(node_name)
调用父类的构造函数,传递节点名称参数,这与Python类似,但语法有所不同。
在构造函数内部,通过this
指针对姓名和年龄进行赋值。
随后是eat
方法的实现,该方法接收食物名称的引用作为参数。方法体中调用了ROS 2的日志模块来输出信息。由于RCLCPP_INFO
采用C风格的格式化输出,因此需要调用c_str()
将name_
和food_name
从字符串类型转换为C风格字符串。
在main
函数中,通过std::make_shared
传入节点名、姓名和年龄,构造一个PersonNode
对象,并将其智能指针赋值给node
。接着调用node
的eat
方法以及ROS 2的相关方法。
5 C++新特性 (必看)
5.1 自动类型推导
自动类型推导体现在代码中就是auto 关键字。我们在实例化ROS 2节点的对象时使用 的就是auto, 它可以在给变量赋值时根据等号右边的类型自动推导变量的类型。
auto node=std::make_shared<rclcpp::Node> ("cpp_node"),
5.2 智能指针
智能指针是C++11引入的一种机制,用于自动管理动态分配的内存,从而避免内存泄漏和空指针等问题。C++11提供了三种智能指针类型:std::unique_ptr
、std::shared_ptr
和std::weak_ptr
。在代码中,std::make_shared
用于创建一个std::shared_ptr
智能共享指针。接下来,我们将重点探讨std::shared_ptr
。
在C语言中,指针用于存储其他变量的地址,而智能指针的功能也类似。然而,当指针指向的动态内存不再需要时,传统指针需要手动调用free
来释放内存。如果忘记释放或过早释放,就会导致内存泄漏或空指针调用等问题。std::shared_ptr
的智能之处在于它能够自动管理内存。它通过引用计数机制记录指向同一资源的指针数量。当引用计数为零时,std::shared_ptr
会自动释放所管理的内存,从而有效避免了提前释放或忘记释放内存的问题。
5.3 Lambda表达式
Lambda表达式是C++11引入的一种匿名函数形式,尽管它没有显式的名字,但可以像普通函数一样被调用。它拥有一套独特的语法结构,其格式如代码所示。
[capture list](parameters)->return type{function body}
其中 ,capture list 表示捕获列表,可以用于捕获外部变量; parameters 表示参数列表; return_type 表示返回值类型; function body 表示函数体。