关于CLION
目前主流的ROS2编辑工具是VSCODE,使用CLION可能比较另类,可能是自己习惯了使用JETBRAIN全家桶,琢磨了一阵后,还是选择了CLION作为在ubuntu上开发ROS2使用的IDE。
CLION本身使用有两个问题,一个是激活码的问题,这个可以在PDD上花几块钱搞定,另一个是搜狗输入法待选框不跟随输入位置的问题,这个有Github大神解决,链接,找到最新的版本下载,解压后覆盖到CLION安装文件的JBR目录下。具体操作步骤见链接,我用的是23.02版本,在链接中查看了下jbr号,还没有出到这个版本,就算下载了最新的文件覆盖后也不起作用,估计作者还没有更新吧。这里还有个土办法,就是将待选框由横向改为纵向,这样虽然待选框是在屏幕左下角,但改为竖向后就可以看到待选文字了,也算是间接解决了这个问题。
其实说是IDE,在ROS2开发中CLION也就是个文本编辑工具,编译和运行以及debug还是手动在命令行中进行的,仅仅对于写代码时的自动补全以及自动缩进使用到了CLION。要想达到自动代码补全功能,主要还是要对CMakeLists.txt文件做好处理。只用CLION打开CMakeLists.txt,在弹出的对话框中选择“以项目打开”,并且在随后弹出的配置选项中,生成器选择“让CMake决定”。这样就能解决大部分自动补全的问题。还有一个需要注意的地方,就是在导入自定义接口的包时,cmake会报错,说找不到XXXXConfig.cmake文件,从而导致这个包的相关文件不能自动补全。这个问题在使用colcon进行编译时不会有问题,就是在CLION中写代码时会很痛苦,解决办法是找到XXXXConfig.cmake文件的目录,一般在install/XXXX/share/XXXX/cmake中(XXXX为自定义的包名),然后在CMakeLists.txt中首行输入set(XXXX_DIR “cmake文件所在绝对路径”),注意一定要在第一行输入,否则不起作用。这样在find(XXXX REQUIRED)语句就不会再报错了,自动补全也没有问题了。
关于ROS2框架的搭建
一般,在主目录下建立工作空间目录,如命名为ros2_ws,再在其下建立src目录,这个目录主要是放置各种包的文件,所有包的建立都在这个目录下(XXXX为要建立的包名称)
mkdir -p ros2_ws/src;cd ros2_ws/src
所有的编译都要在工作空间ros2_ws下进行。
colcon build --packages-select XXXX
自定义接口
ROS2中自定义接口比较简单,是有固定的套路的。首先,命令行输入命令创建包
ros2 pkg create XXXX --build-type ament_cmake --license 'Apache-2.0'
cd XXXX
mkdir msg
mkdir srv
.....
然后用CLION打开生成的package.xml,在<buildtool_depend>ament_cmake</buildtool_depend>添加以下三行:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
再打开CMakeLists.txt,在已有的find_package()行下面添加以下两行:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Xxxx.msg"
"srv/Yyyy.srv")
在文件末尾ament_package()语句之上再添加如下语句,以导出供其他程序调用:
ament_export_dependencies(rosidl_default_runtime)
其中,Xxxx.msg和Yyyy.srv"是自定义的接口文件,注意,文件名首字母需要大写。
再在msg目录下创建Xxxx.msg文件,在srv目录下创建Yyyy.srv文件。
文件中变量定义如下:
ROS类型 | IDL类型 |
---|---|
布尔型(bool) | 布尔型(boolean) |
字节型(byte) | 八位字节型(octet) |
32位浮点型(float32) | 浮点型(float) |
64位浮点型(float64) | 双精度浮点型(double) |
8位整型(int8) | 8位整型(int8) |
无符号8位整型(uint8) | 无符号8位整型(uint8) |
16位整型(int16) | 短整型(short) |
无符号16位整型(uint16) | 无符号短整型(unsigned short) |
32位整型(int32) | 长整型(long) |
无符号32位整型(uint32) | 无符号长整型(unsigned long) |
64位整型(int64) | 超长整型(long long) |
无符号64位整型(uint64) | 无符号超长整型(unsigned long long) |
字符串(string) | 字符串(string) |
据说在ROS2中已经不用char类型了,具体有待查证。所有的数字类型在变量后面加[ ]可以变为对应类型的数组,但据我测试,在humble版本中,string类型不可以定义为数组,也就是说不能定义字符串数组。
接口文件的格式如下:
数据格式定义与C++定义结构体一样,即变量类型 变量名称,后面没有分号,下一条换行继续写文件以"—",即三个横杠分隔,上面部分为请求部分,下面部分为应答部分(消息接口没有应答,也就不需要分隔)。下面贴两个简单的示例。
Student.msg
string name
int32 age
float64 height
StringBack.srv
string request_str
---
string response_str
写好后,退回工作空间目录下,进行编译
colcon build
编译成功后,可以用如下语句查看接口内容:
#查看接口列表
ros2 interface list
#查看所有接口包
ros2 interface packages
#查看某一个包下的所有接口
ros2 interface package std_msgs
#查看某一个接口详细的内容
ros2 interface show std_msgs/msg/String
#输出某一个接口所有属性
ros2 interface proto sensor_msgs/msg/Image
至此,自定义通信接口就完成了。
话题通信
ROS2中,关于通信程序的框架都是类似的,只要记住这些,就能把框架先建立起来,再往进填充自己的内容就行,先说话题通信。
发布方
最开始必须的步骤:创建包,以创建一个名为minimal_talker的发布者为例。在工作空间下面的src目录中创建。
ros2 pkg create minimal_talker --build-type ament_cmake --license 'Apache-2.0' --dependencies rclcpp base_interfaces_demo
上面命令指定了创建方式,协议名称,依赖项。但没有增减–node-name选项,因为这样虽然可以自动生成h和cpp文件,但和我们创建文件的思路方法不同,反而还要进行修改,所以就不指定该选项了。
基本思路就是先写库文件,再写主文件,通信的主要实现在库文件中。
具体来说:
1、编写头文件
2、编写头文件对应的实现源文件
3、编写主文件
4、完善package.xml文件
5、完善CMakeLists.txt文件
其实在CLION中,为了更好的利用好自动补全功能,需要微调一下上述步骤,即先在include/XXXX内建立xxx.h和在src中建立xxx.cpp和main.cpp文件。(XXXX是包名,xxx是库文件名称。)然后,在xxx.cpp中先写入一句话 #include “xxx.h”。随后就开始完善package.xml和CMakeLists.txt文件,主要是后者,因为前者基本不需要再编辑了,默认生成的就比较完善了。
关于CMakeLists.txt文件的完善,贴一个示例结合来说。
#[[ 对于自定义接口的使用,第一步,如果用CLION,则需增加接口包的XXXConfig.cmake文件路径,这样CLION才可以识别,
# 但是对于ROS2而言,使用colcon编译,则不需要这句话也能正常运行]]
set(base_interfaces_demo_DIR
"/home/rotga/studyROS/workspace_01/install/base_interfaces_demo/share/base_interfaces_demo/cmake")
cmake_minimum_required(VERSION 3.8)
project(minimal_talker)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 设定C和C++语言标准
if (NOT CMAKE_C_STANDARD)
set(CMAKE_C_STANDARD 99)
endif ()
if (NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif ()
# 设定一些变量,以便下面使用
set(exeFile minimal_talker_exe)
set(libFile minimal_talker_core)
set(dependencies_1 rclcpp)
set(dependencies_2 base_interfaces_demo)
# 包含include目录
include_directories(include/${PROJECT_NAME})
# 添加ROS2必须有的依赖
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
# 对于自定义接口的使用,第二步,增加自定义包的依赖
find_package(base_interfaces_demo REQUIRED)
# 设定可执行目标和库目标
add_executable(${exeFile} src/main.cpp)
add_library(${libFile} src/minimal_talker.cpp)
# 链接各目标所需要的依赖,对于find_package()语句包含进来的包,使用
# ament_target_dependencies()语句更好,可以减少一些使用target_link_libraries()
# 语句潜在的问题。
ament_target_dependencies(${exeFile} ${dependencies_1} ${dependencies_2})
ament_target_dependencies(${libFile} ${dependencies_1} ${dependencies_2})
target_link_libraries(${exeFile} ${libFile})
# 安装目标文件以及包含目录,这样就会在install/minimal_talker下生成这些文件夹以及文件。
install(TARGETS
${libFile}
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
install(TARGETS
${exeFile}
RUNTIME DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY
include/
DESTINATION include/)
# 下面是测试选项,一般用不到,也是ROS2自动生成的。
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
# 增加以下三行命令,导出项目包含目录以及依赖供其他程序调用。
ament_export_include_directories(include)
ament_export_libraries(${libFile})
ament_export_dependencies(${dependencies_1} ${dependencies_2})
# ament_cmake所需要的结束语句。
ament_package()
完成上述修改,则在代码编写时就有了自动补全功能。
.h文件的编写,同样以具体示例进行讲解。
#ifndef MINIMAL_TALKER_MINIMAL_TALKER_H
#define MINIMAL_TALKER_MINIMAL_TALKER_H
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/msg/student.hpp"
namespace ros_beginner
{
class MinimalTalker:public rclcpp::Node
{
public:
MinimalTalker();
~MinimalTalker() = default;
private:
void timer_callback();
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<base_interfaces_demo::msg::Student>::SharedPtr publisher_;
size_t count_;
};
}
#endif //MINIMAL_TALKER_MINIMAL_TALKER_H
就是声明一个类,用于发布消息。首先这个类是继承于rclcpp::Node基类,再在公有属性内定义了构造函数与析构函数,私有属性内定义了一个回调函数,再定义指向指定消息接口的发布方法的共享指针,这个指针是用来接收创建了的发布对象。这里,所有ROS2定义通信相关的东西都要经历这一步,即用一个指针接收创建好的对象地址(对象包括消息发布,消息接收,服务端,客户端等等)。
接下来是实现文件。
#include <chrono>//为了使用定时功能
#include <functional>//为了使用bind()和function()功能
#include <memory>//为了使用shared_ptr和make_shared功能
#include "minimal_talker.h"
using namespace std::chrono_literals;//不加入此命名空间,则create_wall_timer()不可用
using namespace ros_beginner;//呼应之前定义的命名空间
MinimalTalker::MinimalTalker():Node("minimal_talker"),count_(1)//列表初始化,格式为构造函数名称后加冒号,后跟变量或方法名称,小括号内直接给定初始值。
{//对于非静态成员变量,要在构造函数内初始化,静态成员变量不可在此初始化,要在静态成员函数内初始化。
//重要的步骤,创建话题发布者要用此格式,rclcpp::Node的基类或子类指针->create_publisher<通信接口>("话题名称",缓存队列长度的正整数)。
publisher_ = this->create_publisher<base_interfaces_demo::msg::Student>("chatter",10);
//创建定时器,clcpp::Node的基类或子类指针->create_wall_timer(时间间隔,回调函数指针)。时间间隔是数字加单位,ms或s。回调函数最好用bind()封装起来。
/*bind(封装的回调函数指针,参数列表),对于普通函数,直接写入函数名称即可,对于类的成员函数,
需要按照下面的格式填写两个参数从而把this指针传入(个人理解:类的非静态函数会给每个对象拷贝一份,所以除了函数指针,还需要对象指针才能对函数进行定位),后面再跟回调函数的参数,
参数可以写死,即直接写上去,也可以给出占位符std::placeholders::_n,其中,n为具体的数字,分别为1到9。每个占位符出现的位置代表其在bind函数中的位置,而n为其所代表的参数在回调函数中的位置。
与bind()对应,接收bind返回的指针需要用function<返回值类型(参数1类型,参数2类型...)> 变量名 = bind(封装的回调函数指针,参数列表)这样的形式来接收。
这样做以后变量名可以理解为一个新的函数名称,他后面跟括号就和其他函数一样的使用。
返回值是与回调函数的返回值一致,参数的个数与占位符的数量要一致,注意,这里的个数不包括已经写死的参数数量,仅仅是占位符的数量。顺序与占位符的顺序一致。*/
timer_ = this->create_wall_timer(1s,std::bind(&MinimalTalker::timer_callback,this));
}
void MinimalTalker::timer_callback()
{
//实例化接口
auto message = base_interfaces_demo::msg::Student();
//对接口的信息赋值
message.name = "张三";
message.age = 18;
message.height = 1.83;
//发布消息
publisher_->publish(message);
//将发布的消息打印在屏幕上
RCLCPP_INFO_STREAM(get_logger(),
"第" << count_ << "次发布的消息:" << message.name << "\n"
<< message.age << "\n" << message.height << "\n");
count_++;
}
小结一下,对于消息通信,关键步骤为:
1、继承rclcpp::Node基类编写子类
2、创建发布方,用到的是rclcpp::Publisher<接口名>::SharedPtr publisher_ = 对象名->create_publisher<接口名>(“话题名称”,缓存队列数量的正整数);
3、创建消息发布的内容及方式,以定时发送为例,rclcpp::TimerBase::SharedPtr timer_ = 对象名->create_wall_timer(时间间隔,回调函数);
4、将2和3条内容在构造函数内初始化后即可。
接下来是main函数
#include "minimal_talker.h"
int main(int argc,char* argv[])
{
rclcpp::init(argc,argv);//初始化ROS2
rclcpp::spin(std::make_shared<ros_beginner::MinimalTalker>());//旋转节点
rclcpp::shutdown();//结束,回收资源
return 0;
}
根据上面的步骤,即可将发布方创建并持续发送消息。
订阅方
与发布方类似,创建包,添加完善CMakeLists.txt,编写库的头文件和实现文件,编写main函数。这些基本的步骤都大同小异,下面着重说下订阅方不同的地方:
1、创建订阅方不同,是这么个定义:
rclcpp::Subscription<接口文件>::SharedPtr subscription_ = 对象名->create_subscription<接口文件>(“话题名称”,缓存队列长度的正整数,回调函数);
2、不需要主动接收消息,即不需要创建何时何条件接收消息,只需要被动的接收消息即可;
3、回调函数目的作用不同,写法也不同。订阅方的回调函数主要目的是接收到消息内容,并进行处理。下面举个例子。
#include "minimal_listener.h"
using namespace ros_beginner;
using base_interfaces_demo::msg::Student;
MinimalListener::MinimalListener(): rclcpp::Node("minimal_listener")
{
listener_ = this->create_subscription<Student>("chatter",10,
std::bind(&MinimalListener::chatter_callback,
this,std::placeholders::_1));
}
void MinimalListener::chatter_callback(const base_interfaces_demo::msg::Student &student)
{
RCLCPP_INFO_STREAM(get_logger(),"学生姓名: " << student.name << ";学生年龄: "
<< student.age << ";学生身高: " << student.height);
}
可以看出,回调函数的参数就是接口文件的对象,有了这个对象,也就能获得从发布方来的消息,另外,在bind()方法中,给了一个占位符,这个作用就是获得从发布方来的消息内容,有了这个占位符,就能在回调函数被调用时自动获得参数。再提醒一点,话题名称需要发布方和订阅方完全一致,否则无法进行通信。
服务通信
服务端
与上面的话题通信大同小异,看下面的例子。
/*对于服务器的实现,主要是写好两个地方,第一个是回调函数,这个在下面有详细注释,第二个是创建服务,创建好的
* 服务将返回一个指针,由定义好的变量进行接收。格式为:SERVICE指针变量=创建SERVICE。在ROS2中,通信类
* 基本都是这种格式进行创建。指针变量创建为:rclcpp::Service<模板>::SharedPtr 变量名。模板是要进行
* 通信的接口,可以自定义,通信中所有的变量格式都是根据这个接口定义好的。创建服务为:
* var->create_service<模板>(通信名称,回调函数),这里会返回一个服务指针。模板同上,通信名称是客户端
* 和服务端通信的唯一标识,双方要一致才能通信,回调函数的解释见下面。*/
#include "minimal_server.h"
#include <string>
#include <vector>
using namespace ros_beginner;
using base_interfaces_demo::srv::StringBack;
MinimalServer::MinimalServer(): rclcpp::Node("minimal_server")
{
/*创建服务,create_service函数在尖括号内填写要服务的对象名称,即接收和返回的内容都在次服务对象内定义,
* 小括号内两个参数,一个是服务名称,这个名称时客户端和服务器识别服务的唯一标识,第二个是回调函数,其
* 定义为callbakckT &&calback,可以看出是对回调函数返回值的右值引用。在话题通信和服务通信中都有类似的回调
* 函数,对于这种在函数内需要体现的回调函数,一般采用std::bind()来处理,对于bind(),第一个是要包含
* 的回调函数的指针或引用,第二个是对象的地址,类内使用时用this指针代替(当绑定普通的非类的成员函数时,
* 第二个参数可以不写,因为第一个参数就是其地址),后面的参数是回调函数本身需要的参数,注意,有几个参数
* 就要写几个参数,这些参数可以直接写上给定的值,也可以用占位符代替,而且这些参数必须按照回调函数里的顺序
* 进行书写,占位符是std::placeholders::_n,其中,n是1到10的某一个,这个数字代表着bind()返回的函数
* 的参数位置。比如,回调函数第一个参数位置上写上_3,那么就是返回函数的第三个参数对应回调函数的第一个参数
* 。再说说返回函数,用function<返回值(参数列表)> 变量名 这种形式定义。比如下面:
* function<bool(int,float)> f = bind(&Foo::callback,this,32,placeholders::_3.placeholder::_2)
* 其中,callback定义为 bool callback(int var1,float var2,int var3)。这个例子的意思是回调函数
* 的第一个参数被写死,值为32,第二个参数被占位符规定为返回函数的第三个参数,第三个参数被占位符规定为返回
* 函数的第二个参数,其返回值为bool。对于返回函数,其参数数量要与回调函数参数的占位符数量一致,且顺序为
* 占位符的1,2,3,4,以此类推。*/
server_ = this->create_service<StringBack>("string_service",
std::bind(&MinimalServer::onRequest,this,
std::placeholders::_1,
std::placeholders::_2));
}
/*服务器的回调函数需要有两个参数,一个是客户端的请求内容指针,另一个是服务器的响应内容指针*/
void MinimalServer::onRequest(const std::shared_ptr<base_interfaces_demo::srv::StringBack_Request> request,
const std::shared_ptr<base_interfaces_demo::srv::StringBack_Response> response)
{
std::vector<std::string> keyword {"test","exam","score"};
std::vector<std::string> answer {"yes,there will be a test.","can you pass it? it is difficult",
"don't ask me this question again."};
size_t found ;
response->response_str = "i hava received your message : " + request->request_str + "\n";
for (size_t i = 0;i < keyword.size();++i)
{
found = request->request_str.find(keyword[i]);
/*比如,string类型的变量var,var.find(str)是查找括号内str子字符串在字符串var中出现的首个位置,
* 返回值是数字,当没有找到时,返回的是std::string::npos。*/
if (found != std::string::npos)//说明在请求字符串中包含了关键词
{
response->response_str += answer[i];
break;
}
else if (i == keyword.size() - 1)//说明循环到了最后,也没有匹配到关键字
{
response->response_str += "i don't know what you said,get out of here!";
}
}
}
创建服务端的语句为:
rclcpp::Service<接口名称>::SharedPtr server_ = 对象名->create_service<接口名称>(“服务名称”,回调函数);
回调函数需要有两个参数,这个需要注意,别写少了。
客户端
客户端稍微麻烦点,分别给出库头文件,实现文件,main文件
头文件
#ifndef MINIMAL_CLIENT_MINIMAL_CLIENT_H
#define MINIMAL_CLIENT_MINIMAL_CLIENT_H
#include "rclcpp/rclcpp.hpp"
#include "base_interfaces_demo/srv/string_back.hpp"
namespace ros_beginner
{
class MinimalClient:public rclcpp::Node
{
public:
MinimalClient();
~MinimalClient() = default;
bool isConnected();
rclcpp::Client<base_interfaces_demo::srv::StringBack>::FutureAndRequestId setSendInfo(std::string str);
std::shared_ptr<base_interfaces_demo::srv::StringBack::Request> request;
private:
rclcpp::Client<base_interfaces_demo::srv::StringBack>::SharedPtr client_;
};
}
#endif //MINIMAL_CLIENT_MINIMAL_CLIENT_H
实现文件
#include "minimal_client.h"
using namespace ros_beginner;
using namespace std::chrono_literals;//但凡是ROS2里面关于时间的参数可以直接写数字带单位(s或ms)的,都需要这句话
using base_interfaces_demo::srv::StringBack;
MinimalClient::MinimalClient(): rclcpp::Node("minimal_client")
{
client_ = this->create_client<StringBack>("string_service");
request = std::make_shared<StringBack::Request>();
RCLCPP_INFO_STREAM(get_logger(),"客户端已建立,等待连接!");
}
bool MinimalClient::isConnected()
{
while (!client_->wait_for_service(1s))/*client_->wait_for_service(1s)这个需要
using namespace std::chrono_literals;这句话才能正确编译运行。这个函数的意思
是客户端等待连接服务端,等待时间为里面的参数1秒。如果在这个给定的时间内连接上了,就返回
true,否则返回fasle。*/
{
if (!rclcpp::ok())/*rclcpp::ok()的意思是判断程序是否在正确运行,如果正常运行就返回
* true,否则如遇到CTRL+C之类的中断,则返回false。如返回false,则程序会自动销毁对象,回收资源,
* 结束程序,所以进入下面的程序段时,应考虑到对象已被销毁,则不能使用现有类中的成员,否则会报错。*/
{
RCLCPP_ERROR_STREAM(rclcpp::get_logger("rclcpp"),"未能连接到服务器,用户已中断程序,程序退出……");
return false;
}
RCLCPP_INFO_STREAM(this->get_logger(),"正在连接到服务器,请稍候……");
}
RCLCPP_INFO_STREAM(this->get_logger(),"已经连接到服务器!");
return true;
}
rclcpp::Client<StringBack>::FutureAndRequestId MinimalClient::setSendInfo(std::string str)
{
request->request_str = str;
return client_->async_send_request(request);
}
main文件
#include "minimal_client.h"
using namespace ros_beginner;
using base_interfaces_demo::srv::StringBack;
int main(int argc,char* argv[])
{
if (argc !=2)
{
RCLCPP_INFO_STREAM(rclcpp::get_logger("rclcpp"),"输入参数有误,必须输入一个参数!");
return -1;
}
rclcpp::init(argc,argv);
std::shared_ptr<MinimalClient> client = std::make_shared<MinimalClient>();
if (!client->isConnected())
{
RCLCPP_INFO_STREAM(rclcpp::get_logger("rclcpp"),"客户端已被用户强制退出!程序结束!");
return -2;
}
auto result = client->setSendInfo(argv[1]);
if (rclcpp::spin_until_future_complete(client,result) == rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO_STREAM(client->get_logger(),"请求已发送并得到处理!");
RCLCPP_INFO_STREAM(client->get_logger(),"得到的结果为:" << result.get()->response_str);
}
else
{
RCLCPP_INFO_STREAM(client->get_logger(),"请求异常");
}
rclcpp::shutdown();
return 0;
}
可以看出,main函数写的比较复杂,主要是把没法封装在类中的逻辑处理语句写在了main函数中。没法封装的原因是这句话:rclcpp::spin_until_future_complete(client,result),这句话的功能是等待服务端返回数据,里面的两个参数第一个是客户端对象,第二个是rclcpp::Client<接口名称>::FutureAndRequestId对象。客户端对象没法在类中定义,除非另外写一个类来实现main函数中的这些语句。
客户端的实现思路是:
1、创建客户端。语句大同小异,为rclcpp::Client<接口名称>::SharedPtr client_ = 对象名->create_client<接口名称>(“服务名称”);
2、连接到服务端,可以用示例中的isConnected()函数来实现;
3、发送异步通信消息给服务端,并创建FutureAndRequestId对象,可以用示例中的setSendInfo()函数来实现,注意这个发送消息函数应该有参数,且参数要和接口文件定义的一致。
4、调用spin_until_future_complete来接收服务端返回的消息,并进行处理。
除了示例中的注释外,还有一点要注意,rclcpp::Client<接口名称>::FutureAndRequestId::get()方法在一次通信过程中只能使用一次,第二次调用的话会报错。