ROS2入门笔记
ROS2入门,一篇就够了
欢迎阅读本文~
本文是基于古月居ROS2入门教程、鱼香ROS基础入门教程和B站赵虚左老师的课程的学习笔记,同时做了一些更详细的讲解。参考文献/课程如下:
0.ROS2环境搭建
// 略略略~自己动手 XD
0.1 海龟仿真器
// TODO
1.ROS2的整体架构
ROS2(Robot Operating System 2)是一个用于机器人应用开发的开源中间件集合,它提供了强大的工具和库来帮助开发者构建复杂的机器人系统。ROS2的设计着重于提高可扩展性、实时性和系统的健壮性,适用于各种环境,从个人研究到工业应用都非常合适。以下是ROS2的整体架构和主要组件:
1.1 架构层级
ROS2的架构可以分为多个层级,从硬件层到应用层:
- 硬件层:包括机器人的所有物理组件,如传感器、执行器等。
- 驱动层:负责与硬件层交互,提供硬件抽象,使上层应用能够通过统一的接口与各种硬件交互。
- 中间件层:这是ROS2的核心,采用DDS(Data Distribution Service)作为通信中间件,提供节点间的数据传输和消息服务。
- 客户库层(Client Libraries):如
rclcpp和rclpy,分别为C++和Python提供API,让开发者能够使用各自偏好的编程语言来开发应用。 - 应用层:最终的用户应用程序,包括各种算法、控制逻辑和机器人的行为定义。
1.2 关键组件
- 节点(Node):节点是ROS2中的基本执行单元,每个节点都是一个可独立运行的进程。节点可以发布或订阅话题,提供或使用服务,以及响应外部事件。
- 话题(Topic):话题是一种发布/订阅的通信方式,节点可以发布消息到话题或订阅话题以接收消息。
- 服务(Service):服务是一种同步的双向通信方式,客户端节点发送请求到服务,并等待服务端节点的响应。
- 行动(Action):行动是一种建立在服务上的通信方式,用于长时间运行的任务。客户端可以向服务端发送行动目标,并在任务执行过程中接收反馈,最后接收任务完成的结果。
- 参数(Parameter):参数用于配置节点的运行时行为,可动态调整,如修改传感器的采样率等。
这里只是简要介绍一下这些组件,知道即可,我们下文还会详述。
2.工作空间与功能包
2.1 新建工作空间
ROS2的工作空间(WorkSpace)通常由一个顶级目录(如ros2_ws)和一些子目录组成,如下所示:
ros_ws
├── build
│ └── ...
├── install
│ └── ...
├── log
│ └── ...
└── src
└── pkg1
├── CMakeLists.txt
├── include
│ └── pkg1
├── package.xml
└── src
其中ros_ws/src需要我们自己手动创建,其他目录如build、install、log都是编译时自动创建的。这些目录具体的作用如下:
src,代码空间,未来编写的代码、脚本,都需要人为的放置到这里;src下面的pkg1代表我们创建的其中一个功能包。build,编译空间,保存编译过程中产生的中间文件;install,安装空间,放置编译得到的可执行文件和脚本;log,日志空间,编译和运行过程中,保存各种警告、错误、信息等日志。
我们先创建src,终端命令如下:
mkdir -p ~/ros2_ws/src
然后我们cd到src目录下,这样我们就初始化了一个ROS2工作空间,可以开始开发自己的ROS2包了!
在开发结束后,我们可以通过colcon(ROS 2 推荐的构建工具)来构建整个工作空间的项目。
2.2 rosdep与rosdepc
在ROS中,rosdep 是一个非常重要的命令行工具,它用于帮助安装和管理系统依赖。例如库、软件包等都可以称为依赖。当你在开发一个包时,可能会依赖很多第三方库和系统软件包(比如一些硬件接口库、工具库等)。你只需要在包的 package.xml 文件中声明依赖,rosdep 就能通过查找这些依赖项的配置,自动安装正确的版本。
在第一次使用 rosdep 时,需要先初始化:
sudo rosdep init
rosdep update
这个命令会检查工作空间 src 目录下所有包的依赖,并安装它们:
rosdep install --from-paths src --ignore-src -r -y
最后的**--ignore-src**告诉 rosdep 忽略工作空间中的源代码目录(src)。
-r(或 --reinstall)表示如果依赖已经安装过了,rosdep 会重新安装它们。通常情况下,如果某个依赖已经安装,rosdep 会跳过它,但如果加上 -r,即使依赖已经存在,rosdep 也会强制重新安装。
-y(或 --yes)这个选项会自动回答所有的提示为 “yes”。在安装过程中,rosdep 可能会询问你是否确认安装某些依赖,使用 -y 选项可以跳过这些提示,自动执行操作。这对于批量安装时非常有用,因为你不需要手动确认每一个依赖项。
如果你想检查某个包的依赖是否已经安装好,可以运行:
rosdep check <package_name>
查看某个包的具体依赖项:
rosdep resolve <package_name>
而**rosdepc** 是一个类似于 rosdep 的工具,针对中国用户进行了本地化优化。它帮助自动安装工作空间中的依赖项,特别是在处理一些可能在中国访问受限的外部资源时,rosdepc 提供了更加便捷的方式。通过 rosdepc,用户可以初始化和更新其环境,同时安装所需的依赖,类似于 rosdep 的功能,但更适合中国用户的网络环境。
例如,rosdepc 支持的常见命令包括初始化 (rosdepc init) 和更新 (rosdepc update),以及从指定的工作空间路径安装依赖。而且rosdepc和rosdep的语法一摸一样,如下:
rosdepc install --from-paths src --ignore-src -r -y
这个命令会跟 rosdep install 完全相同,只是会使用国内镜像源来加速依赖项的下载。这样的设计旨在帮助用户轻松管理和编译ROS项目,尤其是在网络连接到国际资源可能存在限制的环境中。
2.3 功能包
2.3.1 创建功能包
功能包(Package)是组织和管理代码的基本单位,类似于ROS1中的软件包。功能包可以包含代码、节点、库、数据文件、配置文件等,它们共同实现特定的功能或提供特定的服务。
一个ROS2的工作空间可以包含多个功能包。这是ROS2设计中的基本特性,支持模块化开发,使得每个功能包可以独立完成特定的功能或任务。这种结构便于大型软件项目的管理,将复杂系统分解为更小、更易管理的部分。
- 每个功能包可以有自己的构建配置、依赖关系和运行环境。
- 功能包之间可以相互依赖,一个包的变化可能会影响其他包。
- 使用如
colcon这样的工具,可以同时构建工作空间中的所有功能包,确保跨包依赖关系正确解决并集成。
我们可以通过下面的命令来创建功能包:
ros2 pkg create --build-type ament_cmake <pkg_name>
--build-type参数后面可以加ament_cmake表示使用cpp编码,也可以换成ament_python表示使用python。最后的<pkg_name>表示新建的功能包的名字。
在我们创建好cpp功能包之后,我们会发现功能包下有两个特殊的文件——CMakeLists.txt和package.xml。package.xml主要描述了功能包的元数据以及依赖关系的信息,如包的名称、版本、维护者信息、许可证信息等基础信息以及功能包所需的其他ROS包或外部库的依赖等依赖关系。我们刚才说可以通过rosdepc工具来从指定工作路径安装依赖,就是查找package.xml中描述的依赖。另外一个CMakeLists.txt用于设置一些编译规则,在此不做赘述,有需要的请自行学习。
2.3.2 获取功能包
获取功能包的方式有两种,我们既可以安装现有的功能包,也可以从源码编译安装。
使用ROS2的包管理工具apt(适用于Ubuntu和Debian系统)可以安装已发布的功能包。例如,要安装名为example_interfaces的功能包,可以使用以下命令:
sudo apt update
sudo apt install ros-<ros2-distro>-example-interfaces
其中<ros2-distro>需要替换为你安装的ROS2发行版,如foxy、galactic、humble等。
如果需要安装的功能包没有预编译版本,或者你需要修改源代码,可以从源码编译安装。通常,这涉及到克隆Git仓库,然后在ROS2工作空间中编译:
source /opt/ros/<ros2-distro>/setup.bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
git clone https://github.com/ros2/example_interfaces.git # 以example_interfaces为例
cd ~/ros2_ws
colcon build
source install/setup.bash
2.4 colcon
colcon 是 ROS 2 中的一个构建工具,它负责构建、测试、安装和管理 ROS 2 工作空间中的包。它是 ROS 2 推荐的构建系统,取代了 ROS 1 中的 catkin。colcon 能够自动化处理工作空间中多个包的构建,尤其是当这些包之间存在依赖关系时,它会按正确的顺序进行构建。
我们在完成开发或者想要进行测试的时候,可以在工作空间目录运行colcon build命令来构建该工作空间中的所有包,colcon会识别CMakeLists文件并调用CMake命令来配置该包,一旦 CMake 配置完毕,colcon 会调用 make来实际编译代码。
3.ROS2的通信机制
机器人是一种高度复杂的系统性实现,一个完整的机器人应用程序可能由若干功能模块(功能包)组成,每个功能模块可能又包含若干功能点(节点),不同功能模块、功能点之间需要进行频繁的数据交互。
3.1 通信机制简介
在ROS2中,通信方式虽然有多种,但是不同通信方式的组成要素都是类似的。比如:通信都是双方或多方行为、通信时都需要将不同的通信对象关联(发布/订阅模型中,发布者和订阅者通过话题进行关联;服务/客户端模型中,服务端和客户端通过服务名进行关联)、都有各自的通信模型(例如例如发布/订阅、服务/客户端、动作)、交互数据是也必然涉及到数据载体(话题、服务、动作消息)等等。
3.1.1 节点
在通信时,不论采用何种眼红方式,通信对象的构建都依赖于节点(Node)。
节点通常是单个的可执行程序,对应某单一的功能模块,这些程序可以进行通信、数据处理、决策等任务,即节点是用来执行具体任务进程的单个可执行文件。值得注意的是,节点是可以跨语言的,虽然语言不同,但是节点之间只要接口相同(如订阅了相同的话题等)就可以实现数据的交互。这些节点可以位于不同的计算机,也可以在云上,也就是说节点可以分布在不同的硬件载体上。我们可以通过节点名称来管理节点。另外,一个可执行文件可以包含一个或多个节点。
那么节点和功能包有何关系呢?
简单来说,节点是执行任务的基本单元,例如读取传感器数据、计算路径、控制机器人等。功能包是组织节点的容器,将相关的节点和资源放在一起进行管理。功能包有助于将不同的功能模块分开,确保代码清晰、可维护、可复用。
节点通常用于完成以下几项任务:
- 数据采集:例如,摄像头节点会负责获取图像数据,激光雷达节点会获取距离信息。
- 数据处理:某些节点可能会处理数据,比如图像处理节点、路径规划节点等。
- 控制机器人:有些节点可能直接控制机器人,比如机器人运动控制节点、导航节点等。
- 通信:ROS2节点通过话题(Topics)、服务(Services)和动作(Actions)进行信息交换。
举个例子,假设你有一个机器人,它需要执行以下任务:
- 采集传感器数据(比如温度传感器、摄像头等)。
- 基于这些数据计算机器人的运动。
- 控制机器人的电机以移动。
在这种情况下,你可能会创建几个节点:
- 一个传感器节点来读取传感器数据。
- 一个控制节点来计算如何移动机器人。
- 一个驱动节点来控制机器人的电机。
3.1.2 话题
一个节点可以将消息发布到一个特定的话题上,这个节点就叫做发布者(Publisher)。任何订阅这个话题的节点(订阅者,Subscriber)都可以接收到这些消息。通过话题进行通信时,发布者和订阅者的数量都可以不唯一,支持一对多和多对多的通信模式。同时,话题是松耦合的,也就是说发布者和订阅者不需要知道对方的存在,它们通过同一个话题进行通信,这使得系统的扩展和维护变得更容易。
3.1.3 通信模型
在 ROS 2 中,常见的通信模型有 发布/订阅(Publish/Subscribe)、服务/客户端(Service/Client)、动作(Action)以及 参数(Parameters)等。这些模型帮助开发者在不同的应用场景中实现不同类型的节点间通信。下面是ROS2中常用的四种通信模型:
- 话题通信:是一种单向通信模型,在通信双方中,发布者发布数据,订阅者订阅数据,数据流单向的由发布方传输到订阅方。
- 服务通信:是一种基于请求响应的通信模型,在双方通信中,客户端发送请求数据到服务端,服务端响应结果给客户端。数据是双向交互的请求响应模式。
- 动作通信:是一种带有连续反馈的通信模型,在通信双方中,客户端发送请求到服务端,服务端响应请求结果给客户端,但是在服务端收到请求到产生最终相应的过程中,会发送连续的反馈信息到客户端。
- 参数服务:是一种基于共享的通信模型,在通信双方中,服务端可以设置数据,而客户端可以连接服务端并操作服务端数据,实现了一定数据的共享。
3.1.4 接口
在通信过程中,需要传输数据,就必然涉及到数据载体,即需要以特定的格式传输数据。在ROS2中,数据载体称之为接口,定义了具体的数据载体的文件称为接口文件。接口文件主要包括消息(.msg)、服务(.srv)和动作(.action)三种类型,每种类型的接口文件都有自己的作用和结构。
3.2 话题通信
话题通信是ROS2中使用频率最高的一种通信模式,这种通信模式是基于发布/订阅模式的,也就是说:一个节点发布消息,另一个(或另一些)节点订阅该消息。这里借用在赵虚左老师的课程中举的一个例子:
机器人在执行导航功能,使用的传感器是激光雷达。机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。
在该场景中,就不止一次使用到了话题通信:
- ROS2中需要有一个节点实时发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
- 导航模块计算出运动控制信息并发布给底盘驱动模块,底盘驱动有一个节点订阅运动信息并将其转换成控制电机的脉冲信号。
以此类推,像雷达、摄像头、GPS等一些传感器数据的采集,也都是使用了话题通信,话题通信适用于实时不断更新的数据传输相关的应用场景。
话题通信是一种以发布/订阅模式实现不同节点之间数据传输的通信模型。数据发布对象称为发布者(Publisher),数据订阅对象称为订阅者(Subscriber),发布者和订阅者通过话题(Topic)相关联,发布者将消息发布在话题上,订阅者则从该话题订阅消息,消息的流向是单向的。
话题通信并不是一对一的关系,它可以是一对多也可以是多对多。就是说,同意话题下可以存在多个发布者,也可以存在多个订阅者,这意味着数据会出现及爱哦查传输的情况。
注:赵虚左老师说这是海王遇到了海后,
xswl
综上所述,话题通信一般应用于不断更新的、少逻辑处理的数据传输场景。
3.2.1 CPP实现简单的Publisher和Subscriber
要实现如题的案例,我们首先要分别实现Publisher和Subscriber的代码,接着去编写配置文件CMakeLists.txt和package.xml,最后在工作空间根目录调用colcon来构建功能包。
下面我们来简单看一看Publisher和Subscriber的代码。
// Publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher() : Node("minimal_publisher") //* 调用基类Node的构造函数声明该节点的名称
{
count_ = 0;
//* 创建发布者,同时指定发布话题和QoS
publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
//* 注册一个定时器,每隔500ms调用一次timer_callback回调函数
timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
//* 调用std_msgs::msg::String()的构造函数来创建消息实例message
auto message = std_msgs::msg::String();
//* 设置消息内容,消息尾部加上string类型的count_++
message.data = "Hello, world! " + std::to_string(count_++);
//* 打印程序状态
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
std::shared_ptr<rclcpp::TimerBase> timer_;
std::shared_ptr<rclcpp::Publisher<std_msgs::msg::String>> publisher_;
int count_;
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
//* rclcpp::spin()函数会持续运行某一个节点,直到节点被销毁或者调用rclcpp::shutdown()函数。
auto node = std::make_shared<MinimalPublisher>(); //* 创建一个指向MinimalPublisher类的shared_ptr对象
rclcpp::spin(node); //* 接受一个shared_ptr类型的节点对象来监听其定时器事件
rclcpp::shutdown();
return 0;
}
create_wall_timer()是rclcpp::Node类的一个成员函数,用来创建一个定时器。该定时器会以固定的频率触发指定的回调函数。这个定时器是基于墙钟时间(wall timer),意味着它与系统的时钟同步。该函数的参数有两个,需要传入peroid和callback。period是一个std::chrono::duration类型的时间段(比如500ms),表示定时器每隔peroid时间就会触发一次回调。callback是定时器出发时调用的回调函数,通常是std::function<void()> 类型,也就是说create_wall_timer()函数的回调函数是没有参数和返回值的。在上面的例子中,我们的回调函数是类成员函数,所以需要std::bind(&Publisher::timer_callback,this)将回调函数和this对象关联到一起,确保成员函数的正常调用(因为成员函数指针必须和对象关联到一起才能调用)。下面是create_wall_timer()函数的签名:
rclcpp::TimerBase::SharedPtr
create_wall_timer(
std::chrono::duration<double> period,
std::function<void()> callback
);
在main函数终端rclcpp::spin()函数需要一个指向节点类的shared_ptr智能指针作为参数,该函数会保持节点处于激活状态并不断监听节点的事件。我们在Publisher节点中通过Node::create_wall_timer注册了一个墙定时器,其可以发出被spin()函数监听并执行的事件,从而调用Publisher::timer_callback回调函数。
Publisher::timer_callback回调函数的任务就是加载消息包并发布消息。在Publisher()构造函数中我们已经初始化了一个指向发布<std_msgs::msg::String>类型消息到topic话题上的rclcpp::Publisher类的智能指针,下面我们要在回调函数中构造消息对象message,并编辑其成员变量message.data。一旦消息对象构建完成,我们便可以使用Publisher::publish()函数来发布消息。上文中的RCLCPP_IFNO并不是ROS2中的函数,而是一个宏,由预处理器展开为预定义的代码片段,用于输出日志(只有日志级别允许显示信息级别的日志,get_logger()的返回值才会被打印)。我们通过这个宏来跟踪程序的执行状态,并打印发布的内容。
在完成Publisher.cpp之后,我们可以先编译出来看看是否把消息成功发布到chatter话题上。我们返回工作空间根目录执行下面的命令:
colcon build --packages-select pub_sub //这里的pub_sub改成自己的功能包的名字
构建成功后我们就可以通过source来更新环境变量了:
source ./install/setup.bash
然后我们分别在两个终端执行下面两个命令:
ros2 run pub_sub pub // 最后的pub是可执行文件的名字
ros2 topic echo /chatter // chatter是刚才我们发布的节点的名称
这样我们就可以在echo中看到发布者发布的消息了。
下面我们来实现Subscriber.cpp。
// Subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class Subscriber : public rclcpp::Node
{
public:
Subscriber() : Node("minimal_subscriber"), count_(0)
{
//* 创建订阅者,接收指向std_msgs::msg::String类型的消息的指针
subscription_ = this->create_subscription<std_msgs::msg::String>("chatter", 10, std::bind(&Subscriber::chatter_callback, this, std::placeholders::_1));
}
private:
int count_;
std::shared_ptr<rclcpp::Subscription<std_msgs::msg::String>> subscription_;
//* 回调函数,用于处理接收到的消息
void chatter_callback(const std::shared_ptr<std_msgs::msg::String> message)
{
RCLCPP_INFO(this->get_logger(), "Subscribing to chatter :%s ,No. %d", message->data.c_str(), ++count_);
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<Subscriber>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
大致内容与Publisher.cpp类似,只不过有些地方需要注意。我们创建的subscription_会从chatter主题那里接受指向std_msg::msg::String类型的指针,我们需要把它传递给回调函数,也就是说create_subscription的回调函数是有参数无返回值的,这样我们就需要使用std::placeholders::_1来占位。create_subscription的函数签名如下:
template<typename MessageT>
rclcpp::Subscription<MessageT>::SharedPtr create_subscription(
const std::string & topic,
rclcpp::QoS qos_profile,
std::function<void(std::shared_ptr<MessageT>)> callback);
// 可以看到回调函数是 std::function<void(std::shared_ptr<MessageT>)>类型的
使用相同的方法编译并运行节点,我们就能看到两个节点之间的通信了。