跟ROS相同,ROS 2也是建议创建一个工作空间,方便管理同一个项目的packages,而且也是将package源文件都放入src文件夹中。 创建工作空间:
mkdir -p ~/colcon_ws/src cd colcon_ws/src现在我们先关注 colcon 的编译过程,所以 package 源文件就先借用官网的。
git clone https://github.com/ros2/examples git checkout $ROS_DISTRO # 切换到与本机版本对应的 branch 上 # 电脑上要是有两个ROS系统,需要先 source /opt/ros/eloquent/setup.bash用 colcon 编译下载的 package
cd .. colcon build编译后,产生了 build, install, log 三个新文件夹。 如果要单独编译某一个 package,可以用如下命令:
colcon build --packages-select PACKAGE_NAME如果不希望编译某一个 package,可以在该 package 中创建名为 COLCON_IGNORE 的空文件,colcon 就会忽略掉该 package,不但不编译,连 colcon list 都不显示,这个 package 对 colcon 就是透明的。
完成编译之后,要 source 一下 setup.bash 文件,确保系统能够找到当前编译生成的可执行文件和库:
source install/setup.bash或者将 source 命令放入 .bashrc 文件,这样每次打开 terminal 就可以自动加载路径信息:
echo "source ~/colcon_ws/install/setup.bash" >> ~/.bashrc我自己的.bashrc文件修改如下(针对电脑双ROS): 这里为老版本,使用起来比较方便,但是在部分程序的 build 等步骤会杂糅很多不需要的东西:
# version 1.0 CUR_PATH=$(pwd | cut -d / -f 4) if [[ "$CUR_PATH" == "colcon_ws" ]];then source /opt/ros/eloquent/setup.bash source ~/colcon_ws/install/setup.bash echo 'ROS 2 sourced' else source /opt/ros/melodic/setup.bash source ~/catkin_ws/devel/setup.bash echo 'ROS sourced' fi因此修正后的代码为:
# version 2.0 alias ros1_="source /opt/ros/melodic/setup.bash && source ~/catkin_ws/devel/setup.bash && echo 'ROS sourced' " alias ros2_="source /opt/ros/eloquent/setup.bash && source ~/colcon_ws/install/setup.bash && echo 'ROS 2 sourced'"首先启动一个 publisher
ros2 run examples_rclcpp_minimal_publisher publisher_member_function再启动一个 subscriber
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function终端显示如下: 这里与 ROS 最大的区别是 不需要启动 ROS master 节点,即不需要类似 roscore 的命令。ROS 2 是真正的分布式系统,不需要中心节点,这样系统的鲁棒性更强,不会因为中心节点失效而影响整个系统。
ROS 2 根据命令的作用对象划分成多个类别,其中常用的几个类别:
Commands: launch Run a launch file node Various node related sub-commands param Various param related sub-commands pkg Various package related sub-commands run Run a package specific executable service Various service related sub-commands srv Various srv related sub-commands topic Various topic related sub-commands在调用时都是采用如下命令格式:
ros2 COMMAND ...我们可以对比 ROS 和 ROS 2 中的几个命令,应该很容易找到其中的规律
运行 ROS node - ROS:rosrun <package_name> <executable_name> - ROS 2:ros2 run <package_name> <executable_name>
查看当前运行的 node - ROS:rosrun <package_name> <executable_name> - ROS 2:rosrun <package_name> <executable_name>
命令 ros2 run 从一个 package 中情动可执行文件:
ros2 run <package_name> <executable_name>ros2 node list 将会展示你所有运行着的节点名称:
ros2 node listRemapping 允许你重新分配默认节点属性,像节点名, topic 名,service 名等等; 重新分配节点名 /turtlesim 的方式如下:
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle此时会出现两个节点名:
/turtlesim /my_turtlenode info 的方式可以获取节点的更多信息:
ros2 node info <node_name>topic 不一定是一对一的通信,它也可以是一对多、多对一或者多对多的。
rqt_graph 用来可视化正在改变的 node 和 topic。 要运行 rqt_graph,需打开一个新终端并输入命令:
rqt_graph在新的终端下运行 ros2 topic list 命令会返回在系统内所有当前活跃的 topic:
ros2 topic listros2 topic list -t 会返回相同的 topic 列表,这时候 topic 会额外显示其类型:
ros2 topic list -t差异如下:
ros2 topic list /parameter_events /rosout /turtle1/cmd_vel ros2 topic list -t /parameter_events [rcl_interfaces/msg/ParameterEvent] /rosout [rcl_interfaces/msg/Log] /turtle1/cmd_vel [geometry_msgs/msg/Twist]浏览数据在话题上发布:
ros2 topic echo <topic_name>浏览数据在话题上发布:
ros2 topic echo <topic_name>该命令可以用来直接从命令行向一个 topic 内发布数据:
ros2 topic pub <topic_name> <msg_type> '<args>'<args> 参数是实际数据,需要以 YAML 语法输入,示例如下:
例1: ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"其中 --once 是一个可选参数,其意味着 “发布一个消息然后退出”。
例2: ros2 topic pub --rate 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"这里 --rate 1 代替了 --once 选项,意思是以一个 1Hz 的稳定流发布命令。
使用该命令可以发布数据的速率:
ros2 topic hz /turtle1/pose输出类似如下:
average rate: 59.354 min: 0.005s max: 0.027s std dev: 0.00284s window: 58service 是另一种节点通信方法。service 基于 call-response 模式,与 publisher-subscriber 模式相对。topic 允许节点 suscribe 数据流并获得持续更新,而 service 仅在被 client 指定调用时才会提供数据。 service 的使用方法与 topic 类似。
为了查看参数的节点归属,可以输入如下命令:
ros2 param list就可以看到每个节点的子命名空间,紧接着的是每个节点的参数:
/teleop_turtle: scale_angular scale_linear use_sim_time /turtlesim: background_b background_g background_r use_sim_time需要注意的是,每个节点都有参数 use_sim_time,它并不是独一无二的。
该命令用于获取当前参数的值:
ros2 param get <node_name> <parameter_name>例如探究上述 /turtlesim 内的参数 background_g:
ros2 param get /turtlesim background_g其参数值为:
Integer value is: 86现在就可以知道 background_g 保留一个整数值。
为了在运行时间内改变参数值,使用命令:
ros2 param set <node_name> <parameter_name> <value>例如:
ros2 param set /turtlesim background_r 150可以将一个节点的所有参数值 “dump” 进一个文件以储存,使得之后能使用该命令:
ros2 param dump <node_name>例如,如果想储存节点 /turtlesim 当前的配置参数,可以输入命令:
ros2 param dump /turtlesim终端将会返回消息:
Saving to: ./turtlesim.yaml你将发现在该工作空间目录有一个新文件被写入了。打开该文件,会显示以下内容:
turtlesim: ros__parameters: background_b: 255 background_g: 86 background_r: 150 use_sim_time: false该参数表需要手动加载。
为了使用在上一小节中保存的参数值,输入:
ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>与之前运行可执行文件方式一样,就是多了两个 flags: ‘–ros-args’ 和 ‘–params-file’,之后再接上想要加载的文件。 紧接着上一节的示例,想要读取该参数,需输入命令:
ros2 run turtlesim turtlesim_node --ros-args --params-file ./turtlesim.yamlROS 中常用的通信机制是话题(Topic)和服务(Service),但是在很多场景下,这两种通信机制往往满足不了所有需求。比如用话题发布运动目标,由于话题是单向通信,则需要另外订阅一个话题来获得机器人运动过程中的状态反馈。如果用服务发布运动目标,虽然可获得一次反馈信息,但是对于控制来说数据太少,而且当反馈迟迟没有收到时,就只能傻傻等待,做不了其他事情。 action 是一种类似于 Service 的问答通信机制,不同之处在于 action 带有连续反馈,可以不断反馈任务进度,也可以在任务过程中中止运行(actions are preemptable)。使用 action 发布机器人的运动目标,机器人在收到这个 action 后就开始运动,在运动过程中不断反馈当前的运动状态;过程中也可以随时取消运动,让机器人停止;当机器人完成运动目标后,action 返回任务完成的消息。
为了识别在 ROS 中所有的 action,运行此命令:
ros2 action list记录发布到一个 topic 的消息可使用命令语句:
ros2 bag record <topic_name>也可以同时记录多个话题:
ros2 bag record -o subset /turtle1/cmd_vel /turtle1/pose选项 -o 允许给 bag 文件定义一个独一无二的名称。紧接着的字符串,subset 就是文件名。
该命令用来查看记录好的 bag:
ros2 bag info <bag_file_name>该命令用于播放记录好的 bag:
ros2 bag play <bag_file_name>注意,与 ROS 不同的是,该命令并不会显示播放的时长以及总时长。
在 ROS 2 内,创建一个新的功能包的命令语法为:
ros2 pkg create --build-type ament_cmake <package_name>例如使用语句:
ros2 pkg create --build-type ament_cmake --node-name my_node my_package上述语句多使用了一个可选参数 --node-name,它产生了一个叫 my_node 的 Hello World 类型可执行文件(在src中创建了名为my_node.cpp的文件)。
在 build 的时候要是只想搭建功能包 my_package,可以运行:
colcon build --packages-select my_package一般来说,使用以下命令会更方便:
ros2 pkg create --build-type ament_cmake <pkg-name> --dependencies [deps] # ros2 pkg create --build-type ament_cmake cpp_parameters --dependencies rclcpp--dependencies 后接所需要的依赖项,这样依赖项会被自动添加进 package.xml 和 CMakeLists.txt 内。
使用示例文件 publisher_member_function.cpp
#include <chrono> // C++11日期和时间库 #include <functional> #include <memory> #include <string> #include "rclcpp/rclcpp.hpp" // 包含了 ROS2 系统的大多部分(rclcpp:ROS Client Library for C++) #include "std_msgs/msg/string.hpp" // 这里跟 ROS1 区别不大 using namespace std::chrono_literals; /* This example creates a subclass of Node and uses std::bind() to register a * member function as a callback from the timer. */ // 通过从 rclcpp::Node 继承创建节点类。代码中的每一个this都指代这个node class MinimalPublisher : public rclcpp::Node { public: MinimalPublisher() : Node("minimal_publisher"), count_(0) //节点被命名为了 minimal_publisher,初始化count_为0 { publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10); timer_ = this->create_wall_timer( // 初始化timer_ 500ms, std::bind(&MinimalPublisher::timer_callback, this)); // 一秒执行两次timer_callback } private: void timer_callback() { auto message = std_msgs::msg::String(); message.data = "Hello, world! " + std::to_string(count_++); RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());// 确保每一个被发布的消息都被打印到控制台 publisher_->publish(message); } rclcpp::TimerBase::SharedPtr timer_; rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; size_t count_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); // 初始化 ROS2 rclcpp::spin(std::make_shared<MinimalPublisher>()); // 开始从节点处理数据 rclcpp::shutdown(); return 0; }使用文本编译器打开 package.xml 文件,复制以下命令:
<depend>rclcpp</depend> <depend>std_msgs</depend>这声明了功能包在它的代码执行过程中需要 rclpp 和 std_msgs。
打开文件 CMakeLists.txt,添加依赖项:
find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED)之后,添加可执行文件并命名为 talker 这样就可以使用节点运行 ros2 run:
add_executable(talker src/publisher_member_function.cpp) ament_target_dependencies(talker rclcpp std_msgs)ament_target_dependencies 的用法为:
ament_target_dependencies(<executable-name> [dependencies])该 CMake 宏用来创建可执行节点并且与依赖项关联起来。
最后,添加 install(TARGETS…) 部分,这样 ros2 run 可以找到该可执行文件:
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})为了 install 启动文件和节点,可以使用 install() 宏放置在文件最后,但在宏 ament_package() 之后, 示例如下:
# Install launch files install( DIRECTORY launch DESTINATION share/${PROJECT_NAME} ) # Install nodes install( TARGETS [node-names] DESTINATION lib/${PROJECT_NAME} )在精简了 CMakeLists.txt 后,它的样子如下:
cmake_minimum_required(VERSION 3.5) project(cpp_pubsub) # Default to C++14 if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 14) endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED) add_executable(talker src/publisher_member_function.cpp) ament_target_dependencies(talker rclcpp std_msgs) install(TARGETS talker DESTINATION lib/${PROJECT_NAME}) ament_package()使用示例文件 subscriber_member_function.cpp
#include <memory> #include "rclcpp/rclcpp.hpp" #include "std_msgs/msg/string.hpp" using std::placeholders::_1; class MinimalSubscriber : public rclcpp::Node { public: MinimalSubscriber() : Node("minimal_subscriber") { subscription_ = this->create_subscription<std_msgs::msg::String>( "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1)); } private: void topic_callback(const std_msgs::msg::String::SharedPtr msg) const { RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str()); } rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalSubscriber>()); rclcpp::shutdown(); return 0; }在这里程序与 Publisher 程序基本相同,除了 spin 的含义。对于 publisher 节点,spin 意味着启动计时器,但对于 subscriber 来说,它只是意味着准备在消息到来时接收消息。
msg 和 srv 文件内构造与 ROS1 相同。
创建一个称作 Num.msg 的新文件,里面写入代码:
int64 num该自定义消息,传输一个名为num的64位整数。
创建一个称作 AddThreeInts.srv 的新文件,里面写入代码:
int64 a int64 b int64 c --- int64 sum该自定义消息,它 request 三个名为 a、b 和 c 的整数,并用一个名为 sum 的整数 respond。
要将您定义的接口转换为特定于语言的代码(如c++和Python),以便在这些语言中使用它们,请向CMakeLists.txt添加以下代码行:
find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "msg/Num.msg" "srv/AddThreeInts.srv" )由于接口依赖 rosidl_default_generators 来生成语言指定代码,需要声明一个依赖性:
<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>与之前的程序相比,微调了一些地方:
Publisher: #include <chrono> #include <memory> #include "rclcpp/rclcpp.hpp" #include "tutorial_interfaces/msg/num.hpp" // CHANGE using namespace std::chrono_literals; class MinimalPublisher : public rclcpp::Node { public: MinimalPublisher() : Node("minimal_publisher"), count_(0) { publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // CHANGE timer_ = this->create_wall_timer( 500ms, std::bind(&MinimalPublisher::timer_callback, this)); } private: void timer_callback() { auto message = tutorial_interfaces::msg::Num(); // CHANGE message.num = this->count_++; // CHANGE RCLCPP_INFO(this->get_logger(), "Publishing: '%d'", message.num); // CHANGE publisher_->publish(message); } rclcpp::TimerBase::SharedPtr timer_; rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // CHANGE size_t count_; }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalPublisher>()); rclcpp::shutdown(); return 0; } Subscriber: #include <memory> #include "rclcpp/rclcpp.hpp" #include "tutorial_interfaces/msg/num.hpp" // CHANGE using std::placeholders::_1; class MinimalSubscriber : public rclcpp::Node { public: MinimalSubscriber() : Node("minimal_subscriber") { subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // CHANGE "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1)); } private: void topic_callback(const tutorial_interfaces::msg::Num::SharedPtr msg) const // CHANGE { RCLCPP_INFO(this->get_logger(), "I heard: '%d'", msg->num); // CHANGE } rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE }; int main(int argc, char * argv[]) { rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MinimalSubscriber>()); rclcpp::shutdown(); return 0; } CMakeLists.txt: find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(tutorial_interfaces REQUIRED) # CHANGE add_executable(talker src/publisher_member_function.cpp) ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE add_executable(listener src/subscriber_member_function.cpp) ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE install(TARGETS talker listener DESTINATION lib/${PROJECT_NAME}) ament_package() package.xml: <depend>tutorial_interfaces</depend>使用 ROS2 启动命令运行一个单独的启动文件将会立即启动整个系统——所有节点及其配置。这在程序的运行中是非常方便的。需要注意的是,ROS2 与 ROS1 在 Launch 文件上有很大的不同。ROS1 使用的是 .launch 文件,ROS2 使用的是 .py 文件。 示例文件 turtlesim_mimic_launch.py(版本:Eloquent)
// 引入python的launch模块 from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( package='turtlesim', node_namespace='turtlesim1', node_executable='turtlesim_node', node_name='sim' ), Node( package='turtlesim', node_namespace='turtlesim2', node_executable='turtlesim_node', node_name='sim' ), Node( package='turtlesim', node_executable='mimic', node_name='mimic', remappings=[ ('/input/pose', '/turtlesim1/turtle1/pose'), ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), ] ) ])这里程序创建了三个节点,所有的都来自于 turtlesim 功能包。系统的目标是打开两个 turtlesim 窗口,其中一只乌龟模仿另一只乌龟的移动。 其中,如下语句为launch文件描述的开始:
def generate_launch_description(): return LaunchDescription([ ])下述的两个节点:
Node( package='turtlesim', node_namespace='turtlesim1', node_executable='turtlesim_node', // 可执行程序的名称 node_name='sim' ), Node( package='turtlesim', node_namespace='turtlesim2', node_executable='turtlesim_node', node_name='sim' ),注意,这两个节点之间的唯一区别是它们的 namespace 值。独一无二的 namespace 允许系统在没有 node name 或 topic name 冲突的情况下启动两个模拟器。
在这个系统中,两只海龟都接收到关于同一主题的命令,并发布它们关于同一主题的姿态。如果没有独一无二的 namespace,就没有办法区分属于某个turtle的消息和属于另一个turtle的消息。
最后的一个节点也来自于 turtlesim 功能包,但是有不同的可执行文件:
Node( package='turtlesim', node_executable='mimic', node_name='mimic', remappings=[ ('/input/pose', '/turtlesim1/turtle1/pose'), ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), ] )mimic 的 /input/pose topic 被重映射为了 /turtlesim1/turtle1/pose;/output/cmd_vel话题被重映射为了 /turtlesim2/turtle1/cmd_vel。这意味着 mimic 会订阅 /turtlesim1/sim 的位姿 topic 并重新发布它给 /turtlesim2/sim 的速度命令 topic 来订阅。
这意味着mimic将订阅/turtlesim1/sim的pose主题,并为/turtlesim2/sim的velocity命令主题重新发布要订阅的主题。
综上,大致的节点间通信如下:
Lauch 文件有两种运行方法:
在当前路径下打开: ros2 launch <launch_file_name> # ros2 launch turtlesim_mimic_launch.py 提供功能包名称打开: ros2 launch <package_name> <launch_file_name>默认情况下,它将创建以下目录作为src目录的对等目录:
build:用来存储中间文件。对于每一个功能包,将创建一个子文件夹,如 CMake 被调用的子文件夹。install:每个功能包被配置到的位置。在默认情况下,每一个功能包都会被配置到一个独立的子目录中。log:包含关于每次 colcon 调用的各种日志信息。安装目录是每个包将被安装到的位置。默认情况下,每个包将安装到单独的子目录中。 日志目录包含关于每次colcon调用的各种日志信息。
需要注意的是,与 ROS1 使用到的 catkin 不同,colcon 没有 devel 目录。
在命令行上使用 ros2 pkg create 可以生成一个基本的 CMake 框架,然后将基本构建信息收集到两个文件中:package.xml 和 CMakeLists.txt。 package.xml 必须包含所有依赖项和一些元数据,以便让 colcon 为这个功能包找到正确的构建顺序,在CI中安装所需的依赖项,并为bloom发行版提供信息。 CMakeLists.txt 包含构建+功能包的可执行文件和库 的命令,这将是本届主要的关注点。
一个 ament 功能包的 CMakeLists.txt 的基本框架包含:
cmake_minimum_required(VERSION 3.5) project(my_project) ament_package()参数 project 表示功能包名称,必须要与在 package.xml 内的功能包名称相同。
项目设置由 ament_package() 完成,每个功能包它必须也只能被调用一次。ament_package() 配置 package.xml,注册带 ament 索引的功能包,并为 CMake 搭建配置文件,以便其他使用 find_package 的功能包可以找到它。因为 ament_package() 从 CMakeLists.txt 收集了很多信息,它需要被 CMakeLists.txt 最后调用。尽管可以在调用 install 函数复制文件和目录之后调用 ament_package(),但是将 ament_package() 恒作为最后的调用更简便。
ament_package() 可以被提供的额外参数:
CONFIG_EXTRAS:CONFIG_EXTRAS_POST:有两个主要的目标需要搭建:由 add_library 搭建的库文件以及由 add_executable 搭建的可执行文件。
由于C/ c++中头文件和实现文件的分离,所以并不总是需要将这两个文件作为参数添加到 add_library/ add_executable 中。
以下是建议的最佳做法:
如果您正在构建一个库,请将所有 clinet 应该会使用到的头文件放在一边,因此必须安装到 include 文件夹的子目录中,而所有其他文件(不应该导出的.c/.cpp和头文件)都放在src文件夹中。只有 cpp 文件在 add_library 或 add_executable 调用中被显示引用。允许通过以下方式查找头文件: target_include_directories(my_target PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>)这个命令在 build 期间内和被 installed 时添加了所有在文件夹 ${CMAKE_CURRENT_SOURCE_DIR}/include 内的文件到公共接口。
原则上,如果${CMAKE_CURRENT_SOURCE_DIR}和${CMAKE_INSTALL_DIR}同时调用了include和顶层文件夹,那么使用生成器表达式在这里是没有必要的,但它是非常常见的。
有两种方式来连接功能包和新的依赖项:
第一种为推荐的方式,使用 ament 宏 ament_target_dependencies。举个栗子,假设想连接 my_target 和线性几何库 Eigen3。
find_package(Eigen3 REQUIRED) ament_target_dependencies(my_target Eigen3)这包含了必须的头文件和库文件,并且它们的依赖项会被项目正确找到。它也确保了所有依赖项的 include 目录顺序正确,在使用 overlay 工作空间的时候。
第二种方式是使用 target_link_libraries。
find_package(Eigen3 REQUIRED) target_link_libraries(my_target Eigen3::Eigen)这也将包括必要的头文件、库和它们的依赖关系,但是与 ament_target_dependencies 相比,在使用 overlay 工作区时,它可能不会正确地对依赖关系排序。