ROS2初步
安装ROS2
鱼香yyds
1 | wget http://fishros.com/install -O fishros && bash fishros |
卸载ROS
1 | sudo apt remove ros-foxy-* && sudo apt autoremove |
基础概念:
与ROS1类似,ROS中同样具有节点,工作空间,功能包等概念
节点
每一个节点都负责一个单独的模块。
举个不太恰当的例子:外卖员小哥外卖给主播小姐姐吃,送累了就刷小姐姐直播跳舞,这里外卖小哥和小姐姐都是一个节点,大家共同构成了一个整体,营造出lianghao社会(bushi)
ROS2中的节点也是如此,每一个节点也是只负责一个单独的模块化的功能(比如一个节点负责控制车轮转动,一个节点负责从激光雷达获取数据、一个节点负责处理激光雷达的数据、一个节点负责定位等等)
节点通信(详见)
ROS2中主要有以下四种通信方式:
- 话题-topics
- 服务-services
- 动作-Action
- 参数-parameters
启动节点
需要使用指令:
1 | ros2 run <package_name> <executable_name> |
命令行查看节点信息
这里涉及以下两个概念:
- GUI(Graphical User Interface)就是平常我们说的图形用户界面,大家用的Windows是就是可视化的,我们可以通过鼠标点击按钮等图形化交互完成任务。
CLI(Command-Line Interface)就是命令行界面了,我们所用的终端,黑框框就是命令行界面,没有图形化。
节点相关CLI:
列举几个常用的:
运行节点(1
ros2 run <package_name> <executable_name>Copy to clipboardErrorCopied
查看节点列表(常用):
1
ros2 node listCopy to clipboardErrorCopied
查看节点信息(常用):
1
ros2 node listCopy to clipboardErrorCopied
重映射节点名称
1
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
工作空间&&功能包:
想要找到一个可执行文件(节点)必须依赖于一个功能包,这些包可以统一放在某个工作空间里。
创建工作空间:1
2mkdir -p turtle_ws/src
cd turtle_ws/src功能包:
可以理解为存放节点的容器。
ROS2中功能包根据编译方式的不同分为三种类型。ament_python,适用于python程序
- cmake,适用于C++
ament_cmake,适用于C++程序,是cmake的增强版
功能包获取
安装一般使用
1
sudo apt install ros-<version>-package_name
手动编译:有点麻烦,一般都是需要对包进行修改的时候shiytong
相关指令——ros2pkg
1
2
3
4
5
6create 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 tag1.创建功能包
1
ros2 pkg create <package-name> --build-type {cmake,ament_cmake,ament_python} --dependencies <依赖名字>Copy to clipboardErrorCopied
2.列出可执行文件
列出所有1
ros2 pkg executablesCopy to clipboardErrorCopied
列出某个功能包的
1
ros2 pkg executables turtlesimCopy to clipboardErrorCopied
3.列出所有的包1
ros2 pkg listCopy to clipboardErrorCopied
4.输出某个包所在路径的前缀
1
ros2 pkg prefix <package-name>Copy to clipboardErrorCopied
比如小乌龟
1
ros2 pkg prefix turtlesimCopy to clipboardErrorCopied
5.列出包的清单描述文件
每一个功能包都有一个标配的manifest.xml文件,用于记录这个包的名字,构建工具,编译信息,拥有者,干啥用的等信息。
通过这个信息,就可以自动为该功能包安装依赖,构建时确定编译顺序等
查看小乌龟模拟器功能包的信息。1
ros2 pkg xml turtlesim
colcon:
colcon其是就是个功能包的构建工具,说白了就是编译器。
ros2默认死没有colcon的,所以需要安装1
sudo apt-get install python3-colcon-common-extensions
相关指令
5.1 只编译一个包
1
colcon build --packages-select YOUR_PKG_NAME Copy to clipboardErrorCopied
5.2 不编译测试单元
1
colcon build --packages-select YOUR_PKG_NAME --cmake-args -DBUILD_TESTING=0Copy to clipboardErrorCopied
5.3 运行编译的包的测试
1
colcon testCopy to clipboardErrorCopied
5.4 允许通过更改src下的部分文件来改变install(重要)
(每次调整 python 脚本时都不必重新build了)
1
colcon build --symlink-install
手撸节点test(c++)
由于python的运行效率实在是一言难尽,我们只学习C++_的版本
创建工作空间&& 功能包
1
2
3mkdir -p town_ws/src
cd town_ws/src
ros2 pkg create village_wang --build-type ament_cmake --dependencies rclcppPOP方式编写节点
在village_wang/src中创建wang2.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include "rclcpp/rclcpp.hpp"
int main(int argc, char **argv)
{
/*初始化rclcpp
rclcpp::init(argc, argv);
/*产生一个Wang2的节点*/
auto node = std::make_shared<rclcpp::Node>("wang2");
// 打印一句自我介绍
RCLCPP_INFO(node->get_logger(), "大家好,我是单身狗wang2.");
/* 运行节点,并检测退出信号*/
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}主函数中首先初始化rclcpp,然后新建了一个Node节点的对象,命名为wang2,接着使用rclcpp让这个节点暴露在外面,并检测退出信号(Ctrl+C),检测到退出信号后,就会执行rcl.shutdown()关闭节点。
添加到cmakelists
在CmakeLists.txt最后一行加入下面两行代码。
1
2add_executable(wang2_node src/wang2.cpp)
ament_target_dependencies(wang2_node rclcpp)添加这两行代码的目的是让编译器编译wang2.cpp这个文件,不然不会主动编译。接着在上面两行代码下面添加下面的代码。
1
2
3
4install(TARGETS
wang2_node
DESTINATION lib/${PROJECT_NAME}
)这个是C++比Python要麻烦的地方,需要手动将编译好的文件安装到install/village_wang/lib/village_wang下.
编译运行:
编译节点
1
colcon build
source环境
1
source install/setup.bash
运行节点
1
ros2 run village_wang wang2_node
不出意外,你可以看到王二的自我介绍。
当节点运行起来后,使用ros2 node list 指令来查看现有的节点。OPP方式编写节点
还是在wang2.cpp输入代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
创建一个类节点,名字叫做SingleDogNode,继承自Node.
*/
class SingleDogNode : public rclcpp::Node
{
public:
// 构造函数,有一个参数为节点名称
SingleDogNode(std::string name) : Node(name)
{
// 打印一句自我介绍
RCLCPP_INFO(this->get_logger(), "大家好,我是单身狗%s.",name.c_str());
}
private:
};
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;
}修改cmakelists&&运行
通信
鱼香教程里的举例实在是难以忘却,这里我cv过来
1
2
3
4
5
6
7
8这里的王二和李四两个节点,通过话题来互相通信(传递数据)。
李四节点会创建一个发布者(Publisher)来发布一个话题(艳娘传奇,小鱼取个英文名叫sexy_girl)。单身汉王二节点,他创建了一个订阅者(Subscriber)来订阅李四发布的话题sexy_girl。
那艳娘传奇的内容是什么呢?我们暂且规定为由文字组成的字符串(连插图都没的那种)。
[object Promise]
李四王二通信模型是一个一对一(一个发布者,一个订阅者)的模型,除此之外ROS2中话题通信其实还可以是1对n,n对1,n对n的。话题通讯
规则:
话题名字是关键,发布订阅接口类型要相同,发布的是字符串,接受也要用字符串来接收;
- 同一个人(节点)可以订阅多个话题,同时也可以发布多个话题,就像一本书的作者也可以是另外一本书的读者;
- 同一个小说不能有多个作者(版权问题),但跟小说不一样,同一个话题可以有多个发布者。
相关工具:
rqt_graph:
ROS2作为一个强大的工具,在运行过程中,我们是可以通过命令来看到节点和节点之间的数据关系的。
命令行界面——CLI
返回系统活动所有主题列表
1 | ros2 topic list |
增加消息类型
1 | ros2 topic list -t |
打印实时话题内容
1 | ros2 topic echo /chatter |
查看主题信息
1 | ros2 topic info /chatter |
查看消息类型
1 | ros2 interface show std_msgs/msg/String |
手动发布命令
1 | ros2 topic pub /chatter std_msgs/msg/String 'data: "123"' |
c++实现
创建话题订阅者的一般流程:
将wang2.cpp代码修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47#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;
}使用C++订阅话题,需要添加对应的消息类型头文件:
1
2创建订阅者和发布者时依然使用this->create_subscription和this->create_publisher方法。
C++中创建一个订阅者,需要传入话题类型、话题名称、所要绑定的回调函数,以及通信Qos.
std::bind()
C++的类成员函数不能像普通函数那样用于回调,因为每个成员函数都需要有一个对象实例去调用它。 通常情况下,要实现成员函数作为回调函数:一种过去常用的方法就是把该成员函数设计为静态成员函数(因为类的成员函数需要隐含的this指针 而回调函数没有办法提供),但这样做有一个缺点,就是会破坏类的结构性,因为静态成员函数只能访问该类的静态成员变量和静态成员函数,不能访问非静态的,要解决这个问题,可以把对象实例的指针或引用做为参数传给它。 后面就可以靠这个对象实例的指针或引用访问非静态成员函数。另一种办法就是使用std::bind和std::function结合实现回调技术。(目前还看不太懂)编译运行
1
colcon build --packages-select village_wang
李四
突然发现李四的源码在教程里没有,自己搓了个试试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using std::placeholders::_1;
// 定义一个小说内容的数组
std::string novel[] = {
"第一回:潋滟湖 1 次偶遇胡艳娘",
"第二回:潋滟湖 2 次偶遇胡艳娘",
"第三回:潋滟湖 3 次偶遇胡艳娘",
"第四回:潋滟湖 4 次偶遇胡艳娘",
"第五回:潋滟湖 5 次偶遇胡艳娘"
};
// 定义一个小说内容的索引
int nb = 0;
/*
创建一个类节点,名字叫做WriterNode,继承自Node.
*/
class WriterNode : public rclcpp::Node
{
public:
// 构造函数,有一个参数为节点名称
WriterNode(std::string name) : Node(name)
{
// 打印一句自我介绍
RCLCPP_INFO(this->get_logger(), "大家好,我是%s,我是一名作家!", name.c_str());
// 创建一个发布者来发布小说内容,通过名字sexy_girl
pub_novel = this->create_publisher<std_msgs::msg::String>("sexy_girl", 10);
// 创建一个定时器,每隔五秒发布一章小说内容
timer = this->create_wall_timer(std::chrono::seconds(5), std::bind(&WriterNode::timer_callback, this));
}
private:
// 声明一个发布者(成员变量),用于发布小说内容
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_novel;
// 声明一个定时器(成员变量),用于定时发布小说内容
rclcpp::TimerBase::SharedPtr timer;
// 定时器触发的回调函数
void timer_callback()
{
// 新建一个小说内容的消息
std_msgs::msg::String novel_msg;
// 判断小说内容的索引是否超出数组范围
if (nb < sizeof(novel) / sizeof(novel[0]))
{
// 获取小说内容
novel_msg.data = novel[nb];
// 发布小说内容
pub_novel->publish(novel_msg);
// 打印发布的小说内容
RCLCPP_INFO(this->get_logger(), "发布小说内容:%s", novel_msg.data.c_str());
// 小说内容的索引加一
nb++;
}
else
{
// 小说内容已经发布完毕,取消定时器
timer->cancel();
// 打印结束语
RCLCPP_INFO(this->get_logger(), "小说已经完结,感谢大家的支持!");
}
};
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv); // 初始化rclcpp
auto node = std::make_shared<WriterNode>("li4"); // 新建一个节点
rclcpp::spin(node); // 保持节点运行,检测是否收到退出指令(Ctrl+C)
rclcpp::shutdown(); // 关闭rclcpp
return 0;
}使用Ctrl+Shift+5切分一个终端出来,输入下面命令:
1
2source install/setup.bash
ros2 run village_li li4_node发布
C++中创建一个发布者也比较简单,使用this->create_publisher即可创建一个发布者。
1
pub_ = this->create_publisher<std_msgs::msg::UInt32>("sexy_girl_money",10);
这里提供了三个参数,分别是该发布者要发布的话题名称(sexy_girl_money)、发布者要发布的话题类型(std_msgs::msg::UInt32)、Qos(10)
服务和接口
接口:接口其实是一种规范
当接口类型统一的时候,适配显然就不是问题了,大家的服务和响应都是一致的规范格式
常用命令
查看接口列表(当前环境下)
1
ros2 interface list
查看所有接口包
1
ros2 interface packages
查看某一个包下的所有接口
1
ros2 interface package std_msgs
查看某一个接口详细的内容
1
ros2 interface show std_msgs/msg/String
输出某一个接口所有属性
1
ros2 interface proto sensor_msgs/msg/Image
显然,服务和话题的区别在于话题是没有返回的,只是单向的数据传递。而服务是双向的客户端发送,服务端响应。
自定义话题接口
- 新建工作空间
在town_ws的src文件夹下,运行下面的指令,即可完成village_interfaces功能包的创建。
注意,这里包的编译类型我们使用ament_cmake方式。
1 | ros2 pkg create village_interfaces --build-type ament_cmake |

- 新建msg文件和Novel.msg(小说消息)
注意:msg文件开头首字母一定要大写,ROS2强制要求,盲猜应该是为了和类名保持一致
1 | cd village_interfaces |
- 编写Novel.msg内容
我们的目的是给李四的小说每一章增加一张图片,原来李四写小说是对外发布一个std_msgs/msg/String字符串类型的数据。
而发布图片的格式,我们需要采用ros自带的传感器消息接口中的图片sensor_msgs/msg/Image数据类型,所以我们新的消息文件的内容就是将两者合并,在ROS2中可以写做这样:
在msg文件中可以使用#号添加注释。
1 | # 标准消息接口std_msgs下的String类型 |
这种组合结构图如下:
[object Promise]
这个图一共三层,第一层是消息定义层,第二层是ROS2已有的std_msgs,sensor_msgs,其组成关系是由下一层组合成上一层。
最下面一层string、uint8、uint32是ROS2中的原始数据类型,原始数据类型有下面几种,ROS2中所有的接口都是由这些原始数据类型组成。
1 | bool |
- Another way
我们不使用std_msgs/String 而是直接使用最下面一层的string。
1 | # 直接使用ROS2原始的数据类型 |
- 说明
如何知道,std_msgs/String是由基础数据类型string组成的,其实可以通过下面的指令来查看
1 | ros2 interface show std_msgs/msg/String |
结果如下:
1 | string data |
原来std_msgs的String就是包含一个叫变量名为data的string类型变量,这也是在4.2和4.3章节中代码要用.data才能拿到真正的数据的原因:
1 | from std_msgs.msg import String |
最终Novel.msg
1 | # 直接使用ROS2原始的数据类型 |
- 修改Cmakelists.txt
完成了代码的编写还不够,我们还需要在CMakeLists.txt中告诉编译器,你要给我把Novel.msg转换成Python库和C++的头文件。
直接添加下面的代码到CMakeLists.txt即可。
1 | #添加对sensor_msgs的 |
find_package用于查找rosidl_default_generators位置,下面rosidl_generate_interfaces就是声明msg文件所属的工程名字、文件位置以及依赖DEPENDENCIES。
踩坑报告:
- 重点强调一下依赖部分DEPENDENCIES,我们消息中用到的依赖这里必须写上,即使不写编译器也不会报错,直到运行的时候才会出错。
- 而且rosidl_generate_interfaces() 函数必须在 ament_package() 函数之前调用。
代码大概是这样的
- 修改package.xml
修改village_interfaces目录下的package.xml,添加下面三行代码,为工程添加一下所需的依赖。
这里其实不添加也可以
1 | <depend>sensor_msgs</depend> |
代码位置:

- 编译
回到town_ws
1 | colcon build --packages-select village_interfaces |
- 验证
过上节课说过的ros2 interface常用的命令来测试。
1 | source install/setup.bash |
我们可以在运行结果中看到,Novel的消息内容是由content数据和传感器数据image共同组成的了。
服务:
- 服务:客户端发送请求给服务端,服务端可以根据客户端的请求做一些处理,然后返回结果给客户端。
启动服务端
运行一个服务节点
1 | ros2 run examples_rclpy_minimal_service service |
查看服务列表
1 | ros2 service list |
手动调用服务(一定要注意a: b: 的空格)
1 | ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}" |
查看服务接口类型
1 | ros2 service type /add_two_ints |
查找使用某一接口的服务
1 | ros2 service find example_interfaces/srv/AddTwoInts |
自定义服务接口
我们来看一下服务的消息接口长什么样子?
服务接口格式:xxx.srv
1 | int64 a |
与话题不同的是,srv文件比msg文件中间多出了三个—-这三个杠杠就是分界线,上方的是客户端发送请求的数据结构定义,下方的是服务端响应结果的数据结构定义。
参考下面的步骤:
- 新建srv文件夹,并在文件夹下新建xxx.srv
- 在xxx.srv下编写服务接口内容并保存
- 在CmakeLists.txt添加依赖和srv文件目录
- 在package.xml中添加xxx.srv所需的依赖
- 编译功能包即可生成python与c++头文件
当然在做上面的步骤之前,我们还需要做一件很重要的事情。就是根据业务需求,确定好请求的数据结构和返回的数据结构。我们依然是在village_interfaces下创建服务接口。
开始之前,我们先根据李四的需求来确定数据结构。
上一节中李四对借钱的要求如下:
- 借钱一定要打欠条,收到欠条才能给钱
- 每次借钱不能超过自己全部资金的10%且一定是整数,也就是说李四假如现在有100块钱,那么最多借出去100x10%=10块钱
总结一下就是,李三发送借钱请求的时候一定要有欠条,我们想一下,欠条中应该至少包含两条信息
- 借钱者名字,字符串类型、可以用string表示
- 金额,整形,可以用uint32表示
那请求的数据结构我们就可以确定下来了,接着确定返回的数据的格式。
既然是借钱,那李四就有可能拒绝,会有借钱失败的情况,所以返回数据应该有这两条信息:
- 是否出借:只有成功和失败两种情况,布尔类型(bool)可表示
出借金额:无符号整形,可以用uint32表示,借钱失败时为0。
创建srv文件夹及BorrowMoney.srv消息文件
编写文件内容
1
2
3
4
5string name
uint32 money
---
bool success
uint32 money修改Cmakelists.txt
我们已经添加过依赖DEPENDENCIES和msg文件了,所以这里我们直接添加一个srv即可。
1
2
3
4
5
6
7
8find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
#---msg---
"msg/Novel.msg"
#---srv---
"srv/BorrowMoney.srv"
DEPENDENCIES sensor_msgs
)需要关注的是这一行”srv/BorrowMoney.srv”,添加了对应的文件位置。
踩坑:在rosidl_generate_interfaces()函数中传递了一个依赖项sensor_msgs,但是在使用find_package()函数之前没有找到它。需要在CMakeLists.txt 文件中添加find_package(sensor_msgs REQUIRED),以确保 sensor_msgs 包被正确地找到和链接。
修改package.xml
1
2
3
4
5<build_depend>sensor_msgs</build_depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>编译
1
colcon build --packages-select village_interfaces
测试
这次测试我们依然使用ros2 interface指令进行测试。
1
2
3
4source install/setup.bash
ros2 interface package village_interfaces
ros2 interface show village_interfaces/srv/BorrowMoney
ros2 interface proto village_interfaces/srv/BorrowMoney服务的c++实现
一句话:张三拿多少钱钱给王二,王二凑够多少个章节的艳娘传奇给他,可参考以下步骤
- 导入服务接口
- 创建服务端回调函数
- 声明并创建服务端
- 编写回调函数逻辑处理请求
添加接口&&依赖
因为village_wang的包类型是ament_cmake,故需要进行以下两步操作:
第一步修改package.xml
加入下面的代码(告诉colcon,编译之前要确保有village_interfaces存在)1
<depend>village_interfaces</depend>
第二步修改和CMakeLists.txt
在CMakeLists.txt中加入下面一行代码find_package是cmake的语法,用于查找库。找到后,还需要将其和可执行文件链接起来1
find_package(village_interfaces REQUIRED)
所以还需要修改ament_target_dependencies,在其中添加village_interfaces。对于C++来说,添加服务接口只需在程序中引入对应的头文件即可。1
2
3
4ament_target_dependencies(wang2_node
rclcpp
village_interfaces
)
这个头文件就是我们SellNovel.srv生成的头文件1
声明回调函数
添加完服务接口接着就可以声明一个卖书请求回调函数。1
2
3
4
5// 声明一个回调函数,当收到买书请求时调用该函数,用于处理数据
void sell_book_callback(const village_interfaces::srv::SellNovel::Request::SharedPtr request,
const village_interfaces::srv::SellNovel::Response::SharedPtr response)
{
}
再创建一个队列,用于存放自己看过的二手书,创建队列需要用到queue容器,所以我们先用#include1
2//创建一个小说章节队列
std::queue<std::string> novels_queue;
死锁
当张三请求王二买二手书的时候,假如王二手里书的数量不足,王二就等攒够了对应数量的书再返回给张三。
等待攒够章节的操作需要在卖书服务函数中阻塞当前线程,阻塞后王二就收不到李四写的小说了,这样一来就会造成一个很尴尬的情景:
在卖书服务回调函数中等着书库(队列)里小说章节数量满足张三需求,接收小说的程序等着这边的卖书回调函数结束,好把书放进书库(队列)里。
这种互相等待的情况,我们称之为死锁
ROS2默认是单线程的,同时只有一个线程在跑,大家都是顺序执行,你干完我干,一条线下去。
所以为了解决这个问题,我们可以使用多线程,即每次收到服务请求后,单独开一个线程来处理,不影响其他部分。
回调函数组
ROS2中要使用多线程执行器和回调组来实现多线程,我们先在SingleDogNode中声明一个回调组成员变量。
1 | // 声明一个服务回调组 |
最终结果
1 | class SingleDogNode : public rclcpp::Node |
实例化服务端&&编写回调函数处理请求
在ROS2中,回调函数组也是一个对象,通过实例化create_callback_group类即可创建一个callback_group_service的对象。
在SingleDogNode的构造函数中添加下面这行代码,即可完成实例化
1 | callback_group_service_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); |
我们使用成员函数作为回调函数,这里要根据回调函数中参数个数,设置占位符,即告诉编译器,这个函数需要传入的参数个数。
在之前订阅话题的回调函数中,我们已经用到过一次了,因为话题回调只有一个参数,所以只需要一个占位符,这里服务的回调是两个参数,所以要设置两个
1 | using std::placeholders::_1; |
在private:下声明服务端
1 | // 声明一个服务端 |
在构造函数中实例化服务端
1 | // 实例化卖二手书的服务 |
实例化服务端可以直接使用create_service函数,该函数是一个模版函数,需要输入要创建的服务类型,这里我们使用的是
- “sell_novel”服务名称,没啥好说的,要唯一哦,因为服务只能有一个
- std::bind(&SingleDogNode::sell_book_callback,this,_1,_2)回调函数,这里指向了我们2.3.1中我们声明的sell_book_callback
- rmw_qos_profile_services_default 通信质量,这里使用服务默认的通信质量
- callbackgroup_service,回调组,我们前面创建回调组就是在这里使用的,告诉ROS2,当你要调用回调函数处理请求时,请把它放到单独线程的回调组中
编写回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 声明一个回调函数,当收到买书请求时调用该函数,用于处理数据
void sell_book_callback(const village_interfaces::srv::SellNovel::Request::SharedPtr request,
const village_interfaces::srv::SellNovel::Response::SharedPtr response)
{
RCLCPP_INFO(this->get_logger(), "收到一个买书请求,一共给了%d钱",request->money);
unsigned int novelsNum = request->money*1; //应给小说数量,一块钱一章
//判断当前书库里书的数量是否满足张三要买的数量,不够则进入等待函数
if(novels_queue.size()<novelsNum)
{
RCLCPP_INFO(this->get_logger(), "当前艳娘传奇章节存量为%d:不能满足需求,开始等待",novels_queue.size());
// 设置rate周期为1s,代表1s检查一次
rclcpp::Rate loop_rate(1);
//当书库里小说数量小于请求数量时一直循环
while (novels_queue.size()<novelsNum)
{
//判断系统是否还在运行
if(!rclcpp::ok())
{
RCLCPP_ERROR(this->get_logger(), "程序被终止了");
return ;
}
//打印一下当前的章节数量和缺少的数量
RCLCPP_INFO(this->get_logger(), "等待中,目前已有%d章,还差%d章",novels_queue.size(),novelsNum-novels_queue.size());
//rate.sleep()让整个循环1s运行一次
loop_rate.sleep();
}
}
// 章节数量满足需求了
RCLCPP_INFO(this->get_logger(), "当前艳娘传奇章节存量为%d:已经满足需求",novels_queue.size());
//一本本把书取出来,放进请求响应对象response中
for(unsigned int i =0 ;i<novelsNum;i++)
{
response->novels.push_back(novels_queue.front());
novels_queue.pop();
}
}
当收到请求时,先计算一下应该给张三多少书novelsNum,然后判断书库里书的数量够不够,不够则进入攒书程序。如果够或者攒够了就把书放到服务响应对象里,返回给张三。这里我们还需要修改一下话题回调函数,增加了一行代码,将小说放到书库里novels_queue.push(msg->data);1
2
3
4
5
6
7
8
9
10
11
12
13// 收到话题数据的回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg){
// 新建一张人民币
std_msgs::msg::UInt32 money;
money.data = 10;
// 发送人民币给李四
pub_->publish(money);
RCLCPP_INFO(this->get_logger(), "王二:我收到了:'%s' ,并给了李四:%d 元的稿费", msg->data.c_str(),money.data);
//将小说放入novels_queue中
novels_queue.push(msg->data);
};修改main函数
因为我们要让整个程序变成多线程的,所以我们要把节点的执行器变成多线程执行器。
修改一下main函数,新建一个多线程执行器,添加王二节点并spin,完整代码如下:wang2.cpp1
2
3
4
5
6
7
8
9
10
11
12int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
/*产生一个Wang2的节点*/
auto node = std::make_shared<SingleDogNode>("wang2");
/* 运行节点,并检测退出信号*/
rclcpp::executors::MultiThreadedExecutor exector;
exector.add_node(node);
exector.spin();
rclcpp::shutdown();
return 0;
}编译:
1
colcon build --packages-select village_wang
测试
1
2
3
4
5
6
7
8
9source install/setup.bash
ros2 run village_wang wang2_node
source install/setup.bash
ros2@ubuntu:~/code/town_ws$ ros2 service list -t
/sell_book [village_interfaces/srv/SellNovel]
ros2 service call /sell_book village_interfaces/srv/SellNovel "{money: 5}"
source install/setup.bash
ros2 run village_li li4_node
客户端实现?id=_3%e5%ae%a2%e6%88%b7%e7%ab%af%ef%bc%88%e5%bc%a0%e4%b8%89%ef%bc%89%e5%ae%9e%e7%8e%b0)





















