ubuntu中使用CLION开发ROS2之通信入门

关于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()方法在一次通信过程中只能使用一次,第二次调用的话会报错。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值