ROS2 系列文章目录
文章目录
- ROS2 系列文章目录
- 前言
- 1. 安装构建工具colcon
- 1.1 简介
- 1.2 安装colcon
- 2. 创建工作空间
- 2.1 创建一个工作目录
- 3 创建一个C++软件包
- 4. 编写发布者节点
- 4.1 使用wget获取一个模板
- 4.2 代码解读
- 4.2 添加依赖项
- 4.3 修改CmakeLists.txt
- 5. 编写订阅者节点
- 5.1 获取代码
- 5.2 代码解读
- 5.2 添加依赖项
- 5.3 修改CMakeLists.txt
- 6. 编译和运行
- 6.1 检查是否缺少依赖项
- 6.2 编译
- 6.3 编译成功
- 6.4 加载setup.sh
- 6.5 运行
- 7. 参考链接
前言
本文主要介绍如何创建、编写、编译、运行ROS2的C++工程。
开发环境是基于Ubuntu的Linux虚拟机。
本文中很多的学习资源来源于ROS2官网
1. 安装构建工具colcon
1.1 简介
colcon是对 ROS 构建工具catkin_make、catkin_make_isolated、catkin_tools和ament_tools的迭代。
据我初步了解ROS1使用的是catkin_make、catkin_make_isolated、catkin_tools。而ROS2使用的是ament_tools,这带来一些问题就是ROS1和ROS2都属于ROS但是却使用不同的构建工具进行编译。
后面ROS想要做一个通用的构建工具,同时这个工具和特定的所支持的构建系统命名无关,然后就有了现在的colcon。
对于嵌入式开发者来说,可以理解为其是一个在Cmake之上的工具。它能够调用cmake去管理和编译c++功能,也能调用其他类似于Cmake之类的构建系统去进行别的构建。
同时它支持了例如创建ROS2的C++工程以及python工程等之类的功能。
在本文中它的主要作用是创建c++工程以及执行编译。
1.2 安装colcon
sudo apt install python3-colcon-common-extensions
2. 创建工作空间
ROS工作空间是一个具有特定结构的目录,通常有一个“src”子目录,用于存放ROS软件包源代码。
这个工作空间大家可以理解为就是ROS2 约定好的一个工作目录,例如它有一个build目录,是用于存放编译过程产生的信息的。
那么为啥要约定这么一个目录呢?我个人认为有以下几点。
- 按照这种方式去创建目录,更方便我们后续的开发,毕竟是官方提供的建议肯定是有一定依据的。
- 大家想一想如果我们随意按照自己喜欢的风格去创建目录结构,那么使用colcon编译过程中它怎么知道把这些信息放在哪里?如果再自己一点点的去配置不是会很麻烦吗。
2.1 创建一个工作目录
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
在创建一个C++的软件包前,我们需要先手动创建一个工作空间,用于存放ROS2的软件包。通过执行上方的指令我们创建出来一个ros2_ws
的工作空间,其中/ros2_ws/src
是用于存放软件包的。
额外说明
当我们通过colcon进行编译后,将会出现新的文件夹
(可以等编译之后查看)
例如我们通过
git clone https://github.com/ros2/examples src/examples -b rolling
将一个examples存储库克隆到~/ros2_ws/src 目录下。
.
└── src
└── examples
├── CONTRIBUTING.md
├── LICENSE
├── rclcpp
├── rclpy
└── README.md
现在src目录中ROS2的示例代码目录结构。
在工作空间的根目录下,运行
colcon build --symlink-install
构建完成后将会看到如下的目录结构
.
├── build
├── install
├── log
└── src
“build”目录将是存储中间文件的地方。对于每个包,将在其中创建一个子文件夹,例如在该子文件夹中调用 CMake
“install”目录是每个软件包将被安装到的位置。默认情况下,每个软件包将被安装到一个单独的子目录中。
"log" 目录包含关于每次 colcon 调用的各种日志信息。
3 创建一个C++软件包
创建完工作空间 ros2_w 后,我们需要在ros2_w/src/目录下去创建我们自己的C++包
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
进入到ros2_w/src/ 目录,然后执行上述指令。 cpp_pubsub是包名, ament_cmake代表我们创建的是一个使用C++工程
4. 编写发布者节点
当按照上面的操作成功创建出软件包后。
我们需要进入ros2_ws/src/cpp_pubsub/src
目录,这是CMake包中包含可执行文件的源文件所在的目录。
所以我们写的源代码要放到该目录
4.1 使用wget获取一个模板
wget -O publisher_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/rolling/rclcpp/topics/minimal_publisher/lambda.cpp
进入到ros2_ws/src/cpp_pubsub/src
目录,运行上述指令,将从github上拉去一个publisher
的demo,对应文件名为publisher_lambda_function.cpp
如果拉不到可以直接创建一个publisher_lambda_function.cpp
的文件,并将下面的代码拷贝进去
源代码如下所示:
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses a fancy C++11 lambda
* function to shorten the callback syntax, at the expense of making the
* code somewhat more difficult to understand at first glance. */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
auto timer_callback =
[this]() -> void {
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(this->count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
this->publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
private:
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);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
4.2 代码解读
第一段
#include <chrono>
#include <memory>
#include <string>
//上面包含了C++库的标准头文件
#include "rclcpp/rclcpp.hpp" //包含了ROS2系统中最常见的部分接口
#include "std_msgs/msg/string.hpp" //包含了用于发布消息的内置消息类型
using namespace std::chrono_literals;
chrono_literals它引入了 chrono 库中的字面量后缀,使得我们能够直接使用时间字面量(比如 1s, 500ms, 2min 等)
来指定时间常量,而不需要显式地使用
std::chrono::seconds,
std::chrono::milliseconds 等类型
第二段
一个继承自rclcpp::Node的MinimalPublisher新节点类。
后续代码中的this 都是指代该新节点
class MinimalPublisher : public rclcpp::Node
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
在构建MinimalPublisher对象时,会创建一个名为minimal_publisher的节点,
并将count_变量初始化为0
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
创建一个String消息类型的topic,并初始化其队列大小为10
auto timer_callback =
[this]() -> void {
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(this->count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
this->publisher_->publish(message);
};
timer_ = this->create_wall_timer(500ms, timer_callback);
}
声明一个名为timer_callback 的 lambda 函数。
它执行当前对象 this 的按引用捕获,不接受输入参数并返回 void。
timer_callback 函数创建一个 String 类型的新消息,使用所需的字符串设置其数据并发布它。
RCLCPP_INFO 宏确保将每条发布的消息打印到控制台。
最后初始化timer_,使其每500ms调用一次上面的回调函数
private:
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);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
main函数中则是负责初始化让节点运行,并最终关闭ROS2的环境
rclcpp::spin 是一个函数,它将节点传递给它并让节点保持运行,直到程序被显式地关闭
(如用户按下 Ctrl+C)退出。
spin 函数在 ROS 2 中用来不断执行节点中的回调函数并维持节点生命周期,
通常用于订阅回调、定时器回调等。个人觉得spin里面的实现应该是类似于一个while(1){ }
4.2 添加依赖项
返回到 ros2_ws/src/cpp_pubsub
目录
打开package.xml
在ament_cmake
构建工具依赖项后添加一个新的行,并粘贴与我哦们节点的包含语句相对应的以下依赖
<depend>rclcpp</depend>
<depend>std_msgs</depend>
这声明了在构建和执行其代码时,该软件包需要rclcpp
和std_msgs
4.3 修改CmakeLists.txt
-
打开
CMakeLists.txt
文件。在现有的依赖项find_package(ament_cmake REQUIRED)
下面,添加以下行:find_package(rclcpp REQUIRED) find_package(std_msgs REQUIRED)
-
之后添加可执行文件,并将其命名为talker,这样我们就可以使用
ros2 run
运行节点了add_executable(talker src/publisher_lambda_function.cpp) ament_target_dependencies(talker rclcpp std_msgs)
-
最后,添加install(TARGETS…)部分,以便ros2 run可以找到我们的可执行文件
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})
-
最终
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_lambda_function.cpp) ament_target_dependencies(talker rclcpp std_msgs) install(TARGETS talker DESTINATION lib/${PROJECT_NAME}) ament_package()
5. 编写订阅者节点
5.1 获取代码
返回ros2_ws/src/cpp_pubsub/src
以创建下一个节点。在终端中输入以下代码:
wget -O subscriber_lambda_function.cpp https://raw.githubusercontent.com/ros2/examples/rolling/rclcpp/topics/minimal_subscriber/lambda.cpp
确保publisher_lambda_function.cpp
和subscriber_lambda_function.cpp
都存在
如果拉不到可以直接创建一个subscriber_lambda_function.cpp
的文件,并将下面的代码拷贝进去
源代码如下所示:
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
subscription_ =
this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
}
private:
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;
}
5.2 代码解读
第一段
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
在头文件包含这块两边基本上都是相同的含义
第二段
class MinimalSubscriber : public rclcpp::Node
订阅者节点是继承Node 创建一个新的类MinimalSubscriber
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
auto topic_callback =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
subscription_ =
this->create_subscription<std_msgs::msg::String>("topic", 10, topic_callback);
}
订阅者和发布者相比没有了定时器,因为订阅者只是在数据发布到topic时进行响应。
topic_callback”函数接收通过主题发布的字符串消息数据,并使用“RCLCPP_INFO”宏将其简单地写入控制台。
private:
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;
}
main函数中则是负责初始化让节点运行,并最终关闭ROS2的环境
rclcpp::spin 是一个函数,它将节点传递给它并让节点保持运行,直到程序被显式地关闭
(如用户按下 Ctrl+C)退出。
spin 函数在 ROS 2 中用来不断执行节点中的回调函数并维持节点生命周期,
通常用于订阅回调、定时器回调等。个人觉得spin里面的实现应该是类似于一个while(1){ }
5.2 添加依赖项
由于此节点与发布者节点具有相同的依赖项,因此无需向package.xml添加任何新内容。
5.3 修改CMakeLists.txt
重新打开 CMakeLists.txt
文件,并在发布者的条目下方为订阅者节点添加可执行文件和目标
add_executable(listener src/subscriber_lambda_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
6. 编译和运行
6.1 检查是否缺少依赖项
像此次我们依赖了rclcpp
和std_msgs
软件包,当然这两个软件包在安装ROS2时就顺带安装了。
但是后续可能有些其他的软件包需要我们单独安装。
所以我们可以先使用rosdep检查是否缺少依赖项,防止后面编译出错排查大半天。
这个指令要在ros2_ws/目录下执行
rosdep install -i --from-path src --rosdistro rolling -y
6.2 编译
在工作空间的根目录ros2_ws中,编译我们的软件包,
colcon build --packages-select cpp_pubsub
这代表着编译cpp_pubsub
包。 相当于执行了该软件包的cmake并进行make.
编译过程中碰到的问题
编译过程中碰到了这种问题。
这个错误提示我们没有缺少catklink_tools
,这个一般是ROS1中使用。
但是这里为啥会使用到我也不是特别清楚,我们就安装就完事了,缺啥补啥。
执行下方的指令进行安装。
pip3 install catkin_pkg
pip3 install lark
上面的指令最好是能在访问外网的情况下执行,如果没有梯子的话,可能会提示下方的问题
那么可以参考
【Python学习:解决pip总是WARNING: Retrying (Retry(total=4…】
如果上面的方式还不行大家可以检查下自己虚拟机的http和https的代理地址是否正确。
6.3 编译成功
就是编译的有点慢
6.4 加载setup.sh
打开一个新的终端后,要先加载设置文件。设置必要的环境变量,确保ROS2相同能够正常运行。
环境变量包括 ROS 2 的安装路径、库路径、消息和服务的定义路径等。
在ros2_ws 目录下
source ./install/setup.sh
6.5 运行
运行发布节点(新终端)
ros2 run cpp_pubsub talker
运行监听节点(新终端)
ros2 run cpp_pubsub listener
7. 参考链接
Writing a simple publisher and subscriber (C++)【官方教程】
Using colcon to build packages【官方教程】
Python学习:解决pip总是WARNING: Retrying (Retry(total=4…