概念:
节点:
ROS中的每个节点应负责单个模块用途(例如,一个节点用于控制车轮电机,一个用于控制激光测距仪等)。每个节点都可以通过主题、服务、操作或参数向其他节点发送和接收数据。
启动一个节点:
ros2 run <package_name> <executable_name>
一些有关节点的指令:
ros2 node list//查看节点列表(常用)
ros2 node info <node_name>//查看节点信息(常用)
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle//重映射节点名称
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10//运行节点时设置参数
工作空间:
工作空间可以类比于VS中的SolutionDir,是一个目录,包含build、install、log、src四个子文件夹,src保存源码。
功能包:
功能包可以理解为存节点的地方,在ROS2中根据编译方式的不同,可以分为:
ament_python,适用于python程序
cmake,适用于C++
ament_cmake,适用于C++程序,是cmake的增强版
-----------------------------------------------------------------------------------------
操作
创建一个ROS 2工作空间的步骤如下:
- 创建一个新的目录用作工作空间。
- 创建一个
src
子目录。 - 初始化工作空间和ROS 2包。
以下是相应的命令行操作:
# 创建工作空间目录
mkdir -p ros2_workspace/src
# 初始化新的ROS 2包,例如包名为my_robot
cd ros2_workspace/src
ros2 pkg create my_robot --build-type {cmake,ament_cmake,ament_python} --dependencies <依赖名字>
# 编译工作空间
cd ros2_workspace
colcon build
# 源工作空间设置脚本
source install/local_setup.bash
一些指令
create Create a new ROS2 package
executables Output a list of package specific executables
list Output a list of available packages
prefix Output the prefix path of a package
xml Output the XML of the package manifest or a specific tag
实例1:
打印helloworld。
mkdir -p ros2/src
cd ros2/src
ros2 pkg create pkg01_helloworld_cpp --build-type ament_cmake --dependencies rclcpp --node-name helloworld
此处的--node-name 是创建了一个hellolworld的可执行程序
cd ..//回到工作空间下
colcon build
. install/setup.bash //刷新一下
ros2 run pkg01_helloworld_cpp helloworld
完成。
下面将进行节点的创建:
1.在vscode中打开工作空间,在工作空间的src下创建一个功能包
2.在功能包的src下创建一个文件名.cpp, 即创建了节点文件
下面我们将编写节点文件。
1.导入库文件
2.初始化客户端库
3.新建节点对象
4.spin循环节点
5.关闭客户端
// 1. 导入库文件
# include "rclcpp/rclcpp.hpp"
int main(int argc,char **argv)
{
//2.初始化客户端库
rclcpp::init(argc,argv);
//3.使用智能指针创建新的节点对象
auto node = std::make_shared<rclcpp::Node>("ros_1");// 当我们初始化节点的时候要给他一个参数,给节点起一个名字
// ros的打印
RCLCPP_INFO(node->get_logger(),"打印内容");
//4.使用spin循环节点
rclcpp::spin(node);
//5. 关闭客户端库
rclcpp::shutdown();
}
注释:auto node是一个智能的类型,可以是任何形式,int char string.....
spin函数是一个用于节点(Node)的重要函数,它允许节点进入事件循环,以处理ROS 2中的消息和事件。以下是关于spin函数的解释:
事件循环:事件循环是一个节点的核心机制,它使节点能够接收、处理和发布ROS 2中的消息、服务请求和其他事件。ROS 2中的事件循环类似于传统的消息循环或事件循环,它允许节点等待和响应来自其他节点的消息和事件。
spin函数的作用:spin函数是ROS 2节点的主要事件循环函数之一。当你调用节点的spin函数时,节点将进入一个无限循环,等待并处理来自ROS 2通信系统的消息和事件。这包括订阅的消息、服务请求、参数更改等。
消息处理:在事件循环中,节点可以在消息到达时执行相应的回调函数。例如,如果节点订阅了一个话题,当新消息发布到该话题时,节点的回调函数将被调用以处理这些消息。这使得节点能够实时地响应和处理外部事件。
非阻塞:spin函数是非阻塞的,这意味着节点在等待事件发生时不会阻止程序的其他部分运行。节点可以并行处理其他任务,而不必等待消息的到达。
rclcpp实际上就是一个函数库
shared_ptr:可以指向特定类型的对象,用于自动释放所指的对象
make_shared:功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr
;
RCLCPP_INFO:用类似于printf()函数的方式输出日志,这样--->RCLCPP_INFO(A,"%d%c",变量1,变量2);那个A表示什么我还不知道。我觉得应该是日志的地址指针,this->get_logger();
完成上述操作后还需要到cmakelist.txt下进行下一步操作
打开cmakelist.txt后加入下面两句代码:
add_executable(node_01 src/node_01.cpp)
ament_target_dependencies(node_01 rclcpp)
第一句是添加可执行文件,第二句是添加依赖。
再加两句:
install(TARGETS
node_01
DESTINATION lib/${PROJECT_NAME}
)
这两句的作用是将节点放到install目录下,然后在source的时候可以找到
疑难: 一些新的函数例如makeshare,init,spin....需要去深度地了解其背后的使用方法
话题与服务
ROS中的话题(Topic),就是Node之间信息传输的方式。
Topic是一对多的传输方式。即一个话题的发布者,可以由多个订阅者来订阅。
比如,机器人现在左转,现在有一个名为”方向”的发布者,向所有订阅者发布“左转”这条信息。方向控制器作为一个订阅者,收到消息后,就控制系统左转;云台作为一个订阅者,得知消息后,也同时控制摄像头往左转,观测前方是否有障碍。
简单来说就是 :节点之间通过话题来实现信息的交流
话题通信的原则有以下:
1.发布订阅接口要相同,发布的是字符,订阅也需要用字符接口来接收
2.同一个节点可以订阅多个话题,也可以发布发布多个话题
3.同一个话题可以有多个发布者
实验一下
创建一个订阅者;
流程:
1.导入话题的接口类型
2.创建订阅回调函数(理解为:执行回调函数是为了让这个订阅者节点能够一直对发布者响应)
3.声明并创立订阅者(此处有点难理解)
4.编写订阅回调处理逻辑
完整代码如下:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "std_msgs/msg/u_int32.hpp"
using std::placeholders::_1;
using std::placeholders::_2;
/*
创建一个类节点,名字叫做SingleDogNode,继承自Node.
*/
class SingleDogNode : public rclcpp::Node
{
public:
// 构造函数,有一个参数为节点名称
SingleDogNode(std::string name) : Node(name)
{
// 打印一句自我介绍
RCLCPP_INFO(this->get_logger(), "大家好,我是单身狗%s.", name.c_str());
// 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));
}
private:
// 声明一个订阅者(成员变量),用于订阅小说
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;
// 收到话题数据的回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "朕已阅:'%s'", msg->data.c_str());
};
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
/*产生一个Wang2的节点*/
auto node = std::make_shared<SingleDogNode>("wang2");
/* 运行节点,并检测退出信号*/
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
代码个人理解:
// 收到话题数据的回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
};
这段是回调函数的实现,就是当订阅者收到来自发布者的消息后,对其进行反馈。
里面是对其反馈的内容,topic_callback是一个函数,传入的是一个共享指针名为msg
----------------------------------------------------------------------------------------------------------------------
// 声明一个订阅者(成员变量),用于订阅小说
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;
声明是对订阅者进行定义,rclcpp::Subscription<>是一个模板类,里面传入的是订阅话题的类型,所以我们订阅的话题类型是std_msgs::msg::String。其实<>里面传入的通常是类,也就是说string也是一个类。
// 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));
reate_subscription<>是一个创建一个订阅者并且返回的函数,<>里面传入接口类型,()里面传入第一个是话题名字,第二个是队列长度(未了解),第三个是接收到话题的信息后,将信息传递的地方。我们在此处是想要把消息传递给回调函数进行处理,因为不能直接在第三个参数处写topic_callback,所以此处使用了bind函数,bind函数传入的第一个参数是回调函数,第二个参数是这个类的对象,也就是现在这个SingleDogNode类的对象,第三个参数是占位符,对应的数量与回调函数的参数数量一致。
msg->data.c_str()是将话题中的数据转化成c语言的string类型。
--------------------------------------------------------------------------------------------------------------------------
创建一个发布者:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "std_msgs/msg/u_int32.hpp"
using std::placeholders::_1;
using std::placeholders::_2;
/*
创建一个类节点,名字叫做SingleDogNode,继承自Node.
*/
class SingleDogNode : public rclcpp::Node
{
public:
// 构造函数,有一个参数为节点名称
SingleDogNode(std::string name) : Node(name)
{
// 打印一句自我介绍
RCLCPP_INFO(this->get_logger(), "大家好,我是单身狗%s.", name.c_str());
// 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));
// 创建发布者
pub_money = this->create_publisher<std_msgs::msg::UInt32>("sexy_girl_money", 10);
}
private:
// 声明一个订阅者(成员变量),用于订阅小说
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;
// 声明一个发布者(成员变量),用于给李四钱
rclcpp::Publisher<std_msgs::msg::UInt32>::SharedPtr pub_money;
// 收到话题数据的回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
// 新建一张人民币
std_msgs::msg::UInt32 money;
money.data = 10;
// 发送人民币给李四
pub_money->publish(money);
RCLCPP_INFO(this->get_logger(), "朕已阅:'%s',打赏李四:%d 元的稿费", msg->data.c_str(), money.data);
};
};
上述发布者的代码与订阅的代码非常相似,大体的函数都差不多。需要注意的是
// 新建一张人民币
std_msgs::msg::UInt32 money;
money.data = 10;
// 发送人民币给李四
pub_money->publish(money);
这串代码将money定义了一下,然后通过publish函数将其发布出去,因为他是在回调函数中,所以,也就是将money发布给了一开始的发布者,也就是李四。
接口:
接口可以抹平不同编码语言对数据类型的差异
所以什么是接口,它是一种相互关系,只有彼此匹配,才能建立连接。
接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。
为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的,比如这里看到的int32表示32位的整型数,int64表示64位的整型数,bool表示布尔值,还可以定义数组、结构体,这些定义在编译过程中,会自动生成对应到C++、Python等语言里的数据结构。
ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。
- 话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值。
- 服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“—”区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数a和b,应答是求和的结果sum。
- 动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是动作的目标,比如是开始运动,运动的结果,最终旋转的90度是否完成,还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。
自定义接口,为了满足个性化的需求,往往需要我们自己去定义一个接口。
过程:
在工作空间下的src下创建一个新的接口功能包;
进入接口功能包中新建一个msg文件,在msg文件下新建一个Novel.msg文件(首字母大写)
在Novel.msg文件中编写如下:
# 标准消息接口std_msgs下的String类型
std_msgs/String content
# 图像消息,调用sensor_msgs下的Image类型
sensor_msgs/Image image
然后配置camkelist.txt文件,为了将Novel.msg转换成c++,python的头文件
在camkelist.txt中加入:
#添加对sensor_msgs的
find_package(sensor_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
#添加消息文件和依赖
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Novel.msg"
DEPENDENCIES sensor_msgs
)
理解:
第一句的作用是告诉依赖了sensor_msgs要找到这个所在的包;
第二句是找到rosidl_generate_interfaces函数所在的包;该函数就是创建了一个接口;
--------------------------------------------------------------------------------------------------------------------------
服务
服务是什么
ROS程序由多个功能组成;一个功能由多个节点完成;各节点的功能独立,节点与节点间通过不同的通信方式,实现数据的传输,完成功能。即总功能由分功能组成,各分功能由节点的子功能及节点间的通信完成。
服务是一种节点与节点间进行一对一、双向传输的数据通信方式,数据通过服务的请求与响应实现传送。
由客户端向服务端提出请求,然后服务端对客户端进行响应
服务接口与话题借口不一样,因为话题多是单向的,而服务是双向的,要用新的接口格式
创建自定义服务接口:
- 新建
srv
文件夹,并在文件夹下新建xxx.srv
- 在
xxx.srv
下编写服务接口内容并保存 - 在
CmakeLists.txt
添加依赖和srv文件目录 - 在
package.xml
中添加xxx.srv
所需的依赖(如果没有加入新的依赖则不需要这步) - 编译功能包即可生成
python
与c++头文件
按照如下格式写进xxx.srv,---上的是请求的数据类型,下面的的是返回的数据类型。
int64 a
int64 b
---
int64 sum
编写服务端实例:
在李三算计着借钱吃麻辣烫的时候,隔壁王家村王二也在和张家村张三讨价还价着。
原来隔壁张家村穷光蛋张三,想看艳娘传奇,但是买不起正版的,想从王二这边买王二看过的二手书。王二思虑一番后,答应了张三,但他有下面的两点要求:
- 必须一手交钱,一手交货,爱买不买
- 每次给多少钱卖多少章,每章一块钱,如果手里的存货不足,就继续等待
张三一看,虽然一块钱对我来说还是很贵,但想想美腻的艳娘
,必须买。
实现步骤:
-
导入服务接口(int money,string[]novels)
-
创建服务端回调函数
-
声明并创建服务端
-
编写回调函数逻辑处理请求
导入服务接口时要到package.xml下添加依赖,也就是服务端口的依赖。打开package.xml输入:
<depend>village_interfaces</depend>
另外还需要到cmakelist.txt下加入 :(进行配置)
find_package(village_interfaces REQUIRED)
这一步是进行查找包,接下来还需要将其和可执行文件链接起来:
找到ament_target_dependencies改成如下
ament_target_dependencies(wang2_node
rclcpp
village_interfaces
)
完成这步后即可导入服务接口:
#include "village_interfaces/srv/sell_novel.hpp"
声明回调函数:
// 声明一个回调函数,当收到买书请求时调用该函数,用于处理数据
void sell_book_callback(const village_interfaces::srv::SellNovel::Request::SharedPtr request,
const village_interfaces::srv::SellNovel::Response::SharedPtr response)
{
}
理解:该函数传入了一个请求和回答指针,也就是说,当我们的客户端传入了请求的时候,我们也就要回调给客户端一个响应,在此处买书案例中,就是指买书的人提出了请求,那么我们的服务端的人就要使用回调函数处理这个请求,以及给予客户答复,如此一来便完成了回调函数。不理解的地方:为什么参数中有Request,和Response,他们是从哪里来的......
类中声明服务端:
// 声明一个服务端
rclcpp::Service<village_interfaces::srv::SellNovel>::SharedPtr server_;
service<>也是一个模板类,里面的是接口类型。
然后需要一个库来放置旧书,要使用queue,来存放。
加入头文件#include <queue>;然后在类中的私有部分加入如下代码,创建了一个novels_queue库;
std::queue<std::string> novels_queue;
squeue<>中的是传入容器中的数据的类型,此处是传的string,容器命名为noves_squeue;
然后此后出现了死锁,原因是我们在卖书的时候要查看容器中的书够不够,才能进行销售,然而同时地,正在等待卖书环节结束后,我们书的数量才能增加,所以,如果当容器中的书数量不够时,我们的卖书环节无法结束,在这一环节中等待书数量的增多,我们增加书的环节也在等待卖书的环节结束。那么这个互相等待的情况就叫死锁。
解决死锁问题需要用到多线程,一边卖书一边增加旧书的数量