使用 C/C++ Conan 包增强 ROS 机器人开发
当我们想到使用 C 或 C++ 进行开发时,我们立即将其与工业系统和嵌入式设备的编程联系起来。确实,该语言在该领域得到广泛使用,这不可避免地包括其在机器人领域的应用。
在这种情况下,毫无疑问,ROS(机器人操作系统) 是最著名的框架,并且可能是使用最广泛的框架之一。它能够将不同的硬件组件集成到一个系统中,使它们能够无缝地协同工作。这使得机器人开发更加高效,因为它允许在各种机器人应用的项目中重用每个组件的软件。
这就是 Conan 团队欣喜地推出 Conan 与 ROS 集成的原因。我们团队的许多成员都来自工业工程领域,我们对这个领域充满热情,因此能够通过 Conan 为机器人领域做出贡献让我们非常高兴。
我们知道rosdep
是 ROS 生态系统中一个众所周知的工具,它有助于将依赖项安装到系统中。但是,在系统级别管理某些 C 和 C++ 依赖项并不总是最佳方法,我们相信 Conan 提供了一些优势,并且可以作为替代方案引入给希望受益于 Conan 特性和其开源包集合的开发人员。
在这篇文章中,我们将讨论构成 ROS 开发的不同组件,它们的工作原理,以及 Conan 如何集成到这些项目中以管理第三方库。
ROS2 包和工作区的快速入门
让我们从一个简短的概述开始,以帮助那些不太熟悉 ROS2 生态系统的人。
ROS 将代码组织成包,每个包都包含一个package.xml
文件,其中包含一些元数据、可执行文件、库或其他资源(如启动文件)。这些包通常在一个**工作区**中管理,允许开发人员构建和运行他们的机器人应用程序。工作流程的关键组件是
-
**CMake 用于构建**: ROS2 严重依赖 CMake 作为其 C/C++ 包的构建系统,使用 CMakeLists.txt 文件定义构建逻辑。
-
**Ament 构建工具**: Ament 构建工具是一个 CMake 宏和函数框架,它标准化并简化了包级别的任务,例如链接依赖项、安装和导出工件或与 ROS 特定工具(如
rosidl
)集成。 -
**Colcon**: 它是协调工作区内多个包构建的主要构建工具。它能够检查包及其依赖项,并按正确的顺序启动构建。它还可以将额外的包覆盖到它们现有的工作区之上,而不会破坏核心系统。
构建 ROS 包的小示例
对于不熟悉 ROS 的读者,我们想展示如何创建和构建 ROS 包以了解该过程。我们将在后面的示例中展示 Conan 集成。
我们需要一个**安装了 ROS2 Humble 版本的 Linux 环境**。如果您在其他系统上运行,或者仅仅为了方便起见,您还可以使用此 Dockerfile 构建和运行命令
Dockerfile
FROM osrf/ros:humble-desktop
RUN apt-get update && apt-get install -y \
curl \
python3-pip \
git \
ros-humble-nav2-msgs \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install --upgrade pip && pip3 install conan
RUN conan profile detect
CMD ["bash"]
您可以使用
docker build -t conanio/ros-humble .
构建并运行 Docker 镜像,并使用docker run -it conanio/ros-humble
运行它
首先,我们创建一个工作区navigation_ws
文件夹,设置 ROS 安装的环境,并创建一个包
$ mkdir /home/navigation_ws && cd /home/navigation_ws
$ source /opt/ros/humble/setup.bash
$ ros2 pkg create --build-type ament_cmake --node-name navigator navigation_package
这些是工作区中应该包含的文件。如本文所述,您可以检查它们以了解内容。
navigation_ws/
navigation_package/
CMakeLists.txt
package.xml
src/
navigator.cpp
现在我们可以调用colcon
来执行构建
$ colcon build --packages-select navigation_package
Starting >>> navigation_package
Finished <<< navigation_package [1.21s]
Summary: 1 package finished [1.60s]
最后,在执行二进制文件之前,我们必须为可执行文件设置环境(以便它能够找到可能具有的任何共享库),然后我们执行它
$ source install/setup.bash
$ ros2 run navigation_package navigator
hello world navigation_package package
将 Conan 依赖项集成到 ROS 示例中
假设我们想使用 Conan 包含一个外部库。在这种情况下,我们将创建一个导航节点,该节点从yaml
文件中发送位置目标到我们的移动机器人。
此示例的代码可以在https://github.com/conan-io/examples2/tree/main/examples/tools/ros/rosenv/navigation_ws找到
navigation_ws/navigation_package/locations.yaml
locations:
- name: "Kitchen"
x: 3.5
y: 2.0
- name: "Living Room"
x: 1.0
y: -1.0
- name: "Bedroom"
x: -2.0
y: 1.5
我们将使用来自 Conan Center 的yaml-cpp
库。为此,我们需要在项目的 CMakeLists.txt 旁边包含一个 conanfile.txt 文件
navigation_ws/navigation_package/conanfile.txt
[requires]
yaml-cpp/0.8.0
[generators]
CMakeDeps
CMakeToolchain
ROSEnv
如您所见,我们在[requires]
部分列出了我们的依赖项,并且我们还添加了所需的生成器,这些生成器将包中文件的信息(库、头文件、可执行文件等)“翻译”成适合构建系统或使用者环境的文件格式。
-
**CMake 生成器**将生成文件,以便可以使用
find_package()
将yaml-cpp
包包含到我们的项目中。 -
**ROSEnv 生成器**将创建一个包含执行构建所需的环境变量的 shell 脚本。
在 CMakeLists.txt 中,我们需要包含**ROS 客户端库**(rclcpp
和 rclcpp_action
)、ROS nav2_msgs和来自 Conan 的yaml-cpp
库。这是使用每个依赖项的find_package
命令完成的,如下所示
navigation_ws/navigation_package/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(navigation_package)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# ROS dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(nav2_msgs REQUIRED)
# Conan dependencies
find_package(yaml-cpp REQUIRED)
add_executable(navigator src/navigator.cpp)
target_compile_features(navigator PUBLIC c_std_99 cxx_std_17) # Require C99 and C++17
ament_target_dependencies(navigator rclcpp rclcpp_action nav2_msgs yaml-cpp)
install(TARGETS navigator
DESTINATION lib/${PROJECT_NAME})
ament_package()
注意:
如果我们必须将 Conan 包作为传递依赖项传播给依赖于此包的其他 ROS 包(如果navigation_package
是库)
其他 ROS 包
→navigation_package
→yaml-cpp Conan 包
我们可以使用 ament 助手ament_export_dependencies()
导出 Conan 目标,就像我们对普通 ROS 包所做的那样。您可以在我们的文档中阅读更多相关信息:https://docs.conan.org.cn/2/integrations/ros.html
为了执行新的构建,首先我们需要清理工作区
$ rm -rf build install log
现在我们像这样安装yaml-cpp
Conan 包
$ conan install navigation_package/conanfile.txt --build=missing --output-folder=install/conan
======== Input profiles ========
Profile host:
[settings]
arch=x86_64
build_type=Release
compiler=gcc
compiler.cppstd=gnu17
compiler.libcxx=libstdc++11
compiler.version=11
os=Linux
...
======== Computing dependency graph ========
yaml-cpp/0.8.0: Not found in local cache, looking in remotes...
yaml-cpp/0.8.0: Checking remote: conancenter
yaml-cpp/0.8.0: Downloaded recipe revision 720ad361689101a838b2c703a49e9c26
Graph root
conanfile.txt: /navigation_ws/navigation_package/conanfile.txt
Requirements
yaml-cpp/0.8.0#720ad361689101a838b2c703a49e9c26 - Downloaded (conancenter)
======== Computing necessary packages ========
yaml-cpp/0.8.0: Main binary package '8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe' missing
...
yaml-cpp/0.8.0: Found compatible package '13be611585c95453f1cbbd053cea04b3e64470ca': compiler.cppstd=17
Requirements
yaml-cpp/0.8.0#720ad361689101a838b2c703a49e9c26:13be611585c95453f1cbbd053cea04b3e64470ca#971e8e22b118a337b31131ab432a3d5b - Download (conancenter)
======== Installing packages ========
-------- Downloading 1 package --------
yaml-cpp/0.8.0: Retrieving package 13be611585c95453f1cbbd053cea04b3e64470ca from remote 'conancenter'
yaml-cpp/0.8.0: Package installed 13be611585c95453f1cbbd053cea04b3e64470ca
yaml-cpp/0.8.0: Downloaded package revision 971e8e22b118a337b31131ab432a3d5b
======== Finalizing install (deploy, generators) ========
conanfile.txt: Writing generators to /navigation_ws/install/conan
...
conanfile.txt: Generated ROSEnv Conan file: conanrosenv.sh
Use 'source /navigation_ws/install/conan/conanrosenv.sh' to set the ROSEnv Conan before 'colcon build'
conanfile.txt: Generating aggregated env files
conanfile.txt: Generated aggregated env files: ['conanbuild.sh', 'conanrun.sh']
Install finished successfully
使用此安装命令,Conan 执行了一些操作
- 在Conan Center(OSS 包贡献的中央存储库)中**搜索**适合您的配置的包。
- 将包**下载**到您计算机上的本地 Conan 缓存中。
- 在 install/conan 文件夹中**生成**ROS 项目所需的环境和 CMake 文件。
最后,让我们将节点的代码添加到 navigator.cpp 文件中
navigation_ws/my_package/src/navigator.cpp
#include <string>
#include <vector>
#include <rclcpp/rclcpp.hpp>
#include <nav2_msgs/action/navigate_to_pose.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <yaml-cpp/yaml.h>
using NavigateToPose = nav2_msgs::action::NavigateToPose;
class YamlNavigationNode : public rclcpp::Node {
public:
YamlNavigationNode(const std::string &yaml_file_path) : Node("yaml_navigation_node") {
// Create action client
action_client_ = rclcpp_action::create_client<NavigateToPose>(this, "navigate_to_pose");
// Read locations from YAML file
RCLCPP_INFO(this->get_logger(), "Reading locations from YAML...");
if (!loadLocations(yaml_file_path)) {
RCLCPP_ERROR(this->get_logger(), "Failed to load locations.");
return;
}
sendAllGoals();
}
private:
struct Location {
std::string name;
double x;
double y;
};
std::vector<Location> locations_;
rclcpp_action::Client<NavigateToPose>::SharedPtr action_client_;
bool loadLocations(const std::string &file_path) {
try {
YAML::Node yaml_file = YAML::LoadFile(file_path);
for (const auto &node : yaml_file["locations"]) {
Location location;
location.name = node["name"].as<std::string>();
location.x = node["x"].as<double>();
location.y = node["y"].as<double>();
locations_.emplace_back(location);
}
return true;
} catch (const std::exception &e) {
RCLCPP_ERROR(this->get_logger(), "Error parsing YAML: %s", e.what());
return false;
}
}
void sendAllGoals() {
for (const auto &location : locations_) {
RCLCPP_INFO(this->get_logger(), "Sending goal to %s: (%.2f, %.2f)", location.name.c_str(), location.x, location.y);
auto goal_msg = NavigateToPose::Goal();
goal_msg.pose.header.frame_id = "map";
goal_msg.pose.header.stamp = this->now();
goal_msg.pose.pose.position.x = location.x;
goal_msg.pose.pose.position.y = location.y;
goal_msg.pose.pose.orientation.w = 1.0;
action_client_->async_send_goal(goal_msg);
}
RCLCPP_INFO(this->get_logger(), "All goals have been sent.");
}
};
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
if (argc < 2) {
RCLCPP_ERROR(rclcpp::get_logger("yaml_navigation_node"), "Usage: yaml_navigation_node <yaml_file_path>");
return 1;
}
std::string yaml_file_path = argv[1];
std::make_shared<YamlNavigationNode>(yaml_file_path);
rclcpp::shutdown();
return 0;
}
该节点将从 YAML 文件中读取位置,并将它们作为导航目标发送给我们的机器人。
我们可以像往常一样构建我们的 ROS 包,只需在之前源conanrosenv.sh
文件即可。这将设置环境,以便 CMake 可以找到 Conan 生成的文件。
$ source install/conan/conanrosenv.sh
$ colcon build --packages-select navigation_package
Starting >>> navigation_package
Finished <<< navigation_package [16.3s]
Summary: 1 package finished [16.7s]
现在是时候运行我们的可执行文件了
$ source install/setup.bash
$ ros2 run navigation_package navigator navigation_package/locations.yaml
[INFO] [1732633293.207085200] [yaml_navigation_node]: Reading locations from YAML...
[INFO] [1732633293.208468700] [yaml_navigation_node]: Sending Kitchen goal (3.50, 2.00) to robot
[INFO] [1732633293.208949200] [yaml_navigation_node]: Sending Living Room goal (1.00, -1.00) to robot
[INFO] [1732633293.209244600] [yaml_navigation_node]: Sending Bedroom goal (-2.00, 1.50) to robot
[INFO] [1732633293.209548300] [yaml_navigation_node]: All goals have been sent.
结论
Conan 的新 ROS 集成提供了一种简洁的方式将 Conan 包合并到 ROS 包中。只需包含一个 conanfile,您就可以从 Conan Center 或您自己的私有服务器安装所需的库。
此外,与系统包管理器相比,像 Conan 这样的包管理器在 ROS 包的开发过程中提供了关键优势。使用 Conan,您可以**安装不同版本或类型的包,而不会干扰其他项目的依赖项**。您甚至可以**将自己的 Conan 包作为依赖项引入,而不会破坏开发工作流程**。
通过此集成,我们希望进一步改进 ROS 包的开发。您可以在我们的文档中找到有关此集成的更多信息:https://docs.conan.org.cn/2/integrations/ros.html。
如果您有任何反馈,请在Conan 存储库中提交问题,以帮助我们改进开发体验。谢谢!