ros2基础

72 篇文章 ¥89.90 ¥99.00

ros2支持的操作系统

windows,linux等都支持,但是对linux的支持是最好的。

教程

英文官网

英文文档

中文官网

视频教程

小鱼教程

安装

官网安装教程

视频教程安装

ros2安装之后需要重启才能有效。

rqt

ros2的图形用户界面;

ros2下载后的头文件和库路径

头文件:

/opt/ros/humble/include

库:

/opt/ros/humble/lib/

ros2下载之后,需要的库文件,头文件都在/opt/ros/humble下。

节点(可执行文件)

本质上就是一个实现了某个功能的模块;

注意:节点不是可执行文件,我们运行一个节点时,本质上是运行一个可执行文件:

ros run 功能包 可执行文件

节点是可执行文件中的一个类对象;

节点有名称,是我们创建节点时指定的,所以ros2 node list的时候查看到的时节点的名称,不是可执行文件名。

-- 节点,即Node,即一个继承自rclcpp::Node的类。

节点的工作过程

1,(c++)接口初始化;

2,创建节点并初始化;

3,spin运行节点,并检测退出信号;

4,销毁节点并关闭(c++)接口;

创建节点时,传递的参数时节点的名称;

节点指令

运行节点

功能包+功能节点

ros2 run 功能包 功能节点

eg: ros2 run turtlesim turtlesim_node

一个包中可以有很多节点;

显示当前所有正在运行的节点

ros2 node list

显示节点信息

ros2 node info 前缀/节点名

创建功能包

创建c/C++的功能包:
ros2 pkg create --build-type ament_cmake 功能包的名称

ros2 pkg create 功能包名称 --build-type ament_cmake --dependencies rclcpp

创建python的功能包:

ros2 pkg create --build-type ament_python 功能包的名字

作用

1,不同的功能包用于存放不同功能的代码(节点),减少代码之间的耦合性;

2,创建功能包不仅仅是创建功能包目录和功能包下的文件,一个很重要的作用是创建的功能包会在CMakeLists.txt中调用ament_package()接口。

ament_package()

每个功能包都必须执行一次调用,会为该功能生成一个ament索引,以及cmake相关配置文件,以便其他功能包能够通过find_package找到该功能包。由于ament_package()会从CMakeLists.txt文件中收集大量信息,因此它应该是CMakeLists.txt文件中的最后一个调用

来源

所以构建时就会生成这个功能包的相关库,头文件以及.cmake配置文件。

创建功能包的位置

工作空间下的src下。

注意:

工作空间下有一个src--用于存放功能包,功能包下有一个src---用于存放节点;

添加CMakeLists.txt指令

创建功能包,指定依赖节点依赖的包之后,至少需要添加一下代码:

add_executable(topic_sub src/topic_sub.cpp)

ament_target_dependencies(topic_sub rclcpp std_msgs) #链接指定的库文件和头文件

install(TARGETS

topic_pub

DESTINATION lib/${PROJECT_NAME})

创建功能包的指令参数

使用以下指令查看

ros2 pkg create -h

--build-type

指定构建工具.

--dependencies

指定功能包中节点的依赖库(包);

--destination-directory

指定创建功能包的位置。

--node-name

指定节点的名字

工作空间

就是一个目录;

工作空间--》包----》节点(可执行文件)

一个工作空间下可以有多个功能包,一个功能包可以有多个节点存在。

 install中的脚本文件

local_setup.sh

local_setup.bash

setup.sh

setup.bash

这些脚本可以配置环境变量,ros2 run运行节点的之前需要运行这些脚本之一,配置同环境变量,ros2 run才能找到功能包。

rclcpp

链接

ros2的c++客户端接口库;

ROS Client Library for C++;

DDS

ROS 2用于通讯的默认中间件;

话题

概念

话题是ros2节点之间数据发布和订阅的中间对象;

发布者publisher将数据发送到指定的话题,订阅者subscription可以从指定的话题获取publisher发布的数据。

话题只是一个概念,还是正是的一个对象?

作用

单向数据传输。

话题的本质

发布者和订阅者都是类对象;

class Subscription : public SubscriptionBase{}

class Publisher : public PublisherBase{}

话题作为中间对象,在代码层面,话题的本质是:

我们创建发布者的时候,create_publisher<messageT>()中的messageT指定的是话题通信所使用的统一接口类型,而不是话题的类型,话题应该只有名称,没有指定的类型。

话题的创建过程

同一个话题能不能同时使用不同的消息接口

publsher和subscription的对应关系

4种

发布者和订阅者都需要在节点中创建使用。

publisher

create_publisher

需指定模板参数-----消息类型;

eg:

publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);

this是继承于rclcpp::Node的节点类

第二个参数如果传递了,表示发布者的队列大小;

Qos,Qos支持直接指定一个数字,这个数字对应的是KeepLast队列长度。一般设置成10,即如果一次性有100条消息,默认保留最新的10个,其余的都扔掉。

publisher对象类型

rclcpp::Publisher<messageT>::SharedPtr publisher_; // 发布者指针--std::shared_ptr类型

模板参数messageT是ros2中的消息接口类型,这些消息接口类型本质上是结构体;

eg:

rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; // 发布者指针--std::shared_ptr类型

publisher工作流程

1,建立publisher;

2,时机成熟,准备好数据,调用publisher的接口publish()发送数据;

#include <iostream>
#include <chrono>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class PublisherNode:public rclcpp::Node{
public:
    PublisherNode(std::string name):Node(name){
        publisher_=this->create_publisher<std_msgs::msg::String>("topic_01",10);
        timer_=this->create_wall_timer(500ms,std::bind(&PublisherNode::_timer_callback,this));
    }

private:
    void timer_callback(){
        auto msg=std_msgs::msg::String();
        msg.data="hello world!";
_        publisher_->publish(msg);
        RCLCPP_INFO(this->get_logger(),"Pulishing: '%s'",msg.data.c_str());
    }
    rclcpp::TimerBase::SharedPtr timer_{};
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_{};
};

int main(int argc,char** argv){
    //初始化ros2的C++接口
    rclcpp::init(argc,argv);
    //建立节点对象
    auto node=std::make_shared<PublisherNode>("publisher_01");
    //运行节点,检测退出信号
    rclcpp::spin(node);
    //销毁节点并关闭C++接口
    rclcpp::shutdown();

    return 0;
}

subscription

create_subscription

至少要传递三个参数:

要订阅的话题的名字,自己用于存储数据的队列的大小;接收到数据之后需要执行的回调函数。

需指定模板参数-----消息类型;

subscription对象类型

rclcpp::Subscription<messageT>::SharedPtr

模板参数messageT是消息对象类;

eg:

rclcpp::Subscription<std_msgs::msg::String>::SharedPtr

订阅者订阅到的数据以共享指针的方式传递给回调函数。

subscription工作流程

1,建立subscription;

订阅者只需一步就可以完成所有工作,创建的时候就指定接收到数据如何处理;

#include <iostream>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class SubscriptionNode:public rclcpp::Node{
public:
    SubscriptionNode(std::string name):Node(name){
        subscription_=this->create_subscription<std_msgs::msg::String>("topic_01",10,std::bind(&SubscriptionNode::_topic_callback,this,std::placeholders::_1));
    }

private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg){
        RCLCPP_INFO(this->get_logger(),"Subscription heard: %s",msg->data.c_str());
    }
_    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_{};
};

int main(int argc,char** argv){
    rclcpp::init(argc,argv);
    auto node=std::make_shared<SubscriptionNode>("subscription_01");
    rclcpp::spin(node);
    rclcpp::shutdown();

    return 0;
}

订阅者的回调函数接收的数据参数不能使用引用类型:

#include <iostream>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class SubscriptionNode:public rclcpp::Node{
public:
    SubscriptionNode(std::string name):Node(name){
        subscription_=this->create_subscription<std_msgs::msg::String>("topic_01",10,std::bind(&SubscriptionNode::_topic_callback,this,std::placeholders::_1));
    }

private:
    //这里不能使用引用
    void topic_callback(const std_msgs::msg::String::SharedPtr& msg){
        RCLCPP_INFO(this->get_logger(),"Subscription heard: %s",msg->data.c_str());
    }
_    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_{};
};

int main(int argc,char** argv){
    rclcpp::init(argc,argv);
    auto node=std::make_shared<SubscriptionNode>("subscription_01");
    rclcpp::spin(node);
    rclcpp::shutdown();

    return 0;
}

原因:

订阅者如何指定订阅的话题

create_subscription(topic_name)即指定订阅的话题的名称,就是指定需要监听的话题;

发布者和订阅者的队列

这个队列用来干什么?

注意:队列的大小描述的是发送端存储需要发送的msg的队列的大小,以及订阅端订阅到Msg未处理时,msg存储的队列的大小,而不是话题的队列。

publisher

The depth of the publisher message queue.

subscripttion

The depth of the subscription's incoming message queue.

链接

用于存储接收到未处理的msg。

案例:

publisher创建队列大小为10:

从0开始2s发送一次Msg;

#include <iostream>
#include <chrono>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"

using namespace std::chrono_literals;

class PublisherNode:public rclcpp::Node{
public:
    PublisherNode(std::string name):Node(name){
        publisher_=this->create_publisher<std_msgs::msg::Int32>("topic_queue",10);
        timer_=this->create_wall_timer(2s,std::bind(&PublisherNode::_timer_callback,this));
    }

private:
    void timer_callback(){
        auto msg=std_msgs::msg::Int32();
_        msg.data=value_;
        publisher_->publish(msg);
        RCLCPP_INFO(this->get_logger(),"Pulishing: '%d'",msg.data);
        value_++;
    }
private:
    int value_{0};
    rclcpp::TimerBase::SharedPtr timer_{};
    rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr publisher_{};
};

int main(int argc,char** argv){
    //初始化ros2的C++接口
    rclcpp::init(argc,argv);
    //建立节点对象
    auto node=std::make_shared<PublisherNode>("publisher_queue");
    //运行节点,检测退出信号
    rclcpp::spin(node);
    //销毁节点并关闭C++接口
    rclcpp::shutdown();

    return 0;
}

subscription任意时刻开启:

#include <iostream>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/int32.hpp"

class SubscriptionNode:public rclcpp::Node{
public:
    SubscriptionNode(std::string name):Node(name){
        subscription_=this->create_subscription<std_msgs::msg::Int32>("topic_queue",10,std::bind(&SubscriptionNode::_topic_callback,this,std::placeholders::_1));
    }

private:
    void topic_callback(const std_msgs::msg::Int32::SharedPtr msg){
        RCLCPP_INFO(this->get_logger(),"Subscription heard: %d",msg->data);
    }
_    rclcpp::Subscription<std_msgs::msg::Int32>::SharedPtr subscription_{};
};

int main(int argc,char** argv){
    rclcpp::init(argc,argv);
    auto node=std::make_shared<SubscriptionNode>("subscription_01");
    rclcpp::spin(node);
    rclcpp::shutdown();

    return 0;
}

测试:

publisher:

subscription:

结论:

订阅端只能接收到订阅端启动后发送端发送的数据。

虽然发送端和订阅端的队列都不是1,但是因为数据不是直接从发送端的队列读取,而是由发送端发送到话题,再由订阅端从话题读取,而话题只能存储定义的接口类型最新的一个数据,所以订阅端只能获取到最新的一个数据。

话题跨编程语言、跨平台和跨设备

ROS2在数据传递时做好了消息的序列化和反序列化,而且ROS2的消息序列化与反序列化通信是可以做到跨编程语言、跨平台和跨设备之间的。

服务

概念

服务是客户端节点和服务端节点进行数据请求和响应的中间对象;

客户端将数据发送给服务对象,服务端从服务对象获取请求,处理数据之后,服务端将响应传递给服务对象,客户端再从服务对象获取响应数据。

作用

双向数据传输。

服务的本质

客户端和服务端对象都是类对象;

那么服务作为客户端和服务端中的中间通信对象,在代码层面,具体是上面呢?

服务的创建过程

服务的限制

客户端需要进行的工作

1,创建客户端对象;

2,发送请求;

3,监听响应;

4,处理响应;

服务端需要进行的工作

1,创建服务端对象;

2,监听服务中的请求;

3,处理请求并返回响应;

客户端节点创建客户端对象

对象类型

rclcpp::Client<serviceT>::SharedPtr

模板参数serviceT是一个消息接口。

本质上是一个结构体;

这个结构体中有这个接口对应的请求对象和响应对象成员;

eg:

rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr

create_client

至少需要一个参数;

需指定模板参数---服务接口类型;

client发送请求

1.等待服务端上线

为什么要等待服务端上线?

因为要向服务端请求数据,如果服务端没有上线,请求没有意义;

等待的接口:

client_->wait_for_service(timeout)

  template<typename RepT = int64_t, typename RatioT = std::milli>
  bool
  wait_for_service(
    std::chrono::duration<RepT, RatioT> timeout = std::chrono::duration<RepT, RatioT>(-1))
  {
    return wait_for_service_nanoseconds(
      std::chrono::duration_cast<std::chrono::nanoseconds>(timeout)
    );
  }

需要等待的时间,时间到服务端没有上线等待返回;

默认timeout=-1,表示一直等待;

等待失败退出是,需要检测,是否rclcpp状态:

if (!rclcpp::ok()) {

    RCLCPP_ERROR(this->get_logger(), "等待服务的过程中被打断..."); return;
}

2.构造请求的

本质上就是建立指定服务的请求共享指针对象;

eg:

auto request = std::make_shared<example_interfaces::srv::AddTwoInts_Request>();

3.发送异步请求

客户端发送请求,使用create_client()接触创建的对象的成员函数发送;

client_->async_send_request()

发送异步请求,然后等待返回,返回时调用回调函数;

async_send_request()是发送请求,注册回调函数之后就返回,还是等待响应,执行完成回调函数之后才返回?

发送请求,注册之后就立即返回了,应该由内核监测响应并发送中断信号给用户线程,之后用户线程采取执行回调函数。

异步发送接口--async_send_request()

三个版本

接口的返回值

返回的是一个结构体:

FutureAndRequestId

  struct FutureAndRequestId
    : detail::FutureAndRequestId<std::future<SharedResponse>>
  {
    using detail::FutureAndRequestId<std::future<SharedResponse>>::FutureAndRequestId;

    /// Deprecated, use `.future.share()` instead.
    /**
     * Allow implicit conversions to `std::shared_future` by value.
     * \deprecated
     */
    [[deprecated(
      "FutureAndRequestId: use .future.share() instead of an implicit conversion")]]
    operator SharedFuture() {return this->future.share();}

    // delegate future like methods in the std::future impl_

    /// See std::future::share().
    SharedFuture share() noexcept {return this->future.share();}
  };

返回的FutureAdnRequestId结构体继承于FutureAndRequestId<std::future<SharedResponse>>:

namespace detail
{
template<typename FutureT>
struct FutureAndRequestId
{
  FutureT future;
  int64_t request_id;

  FutureAndRequestId(FutureT impl, int64_t req_id)
  : future(std::move(impl)), request_id(req_id)
  {}

  /// Allow implicit conversions to `std::future` by reference.
  operator FutureT &() {return this->future;}

  /// Deprecated, use the `future` member variable instead.
  /**
   * Allow implicit conversions to `std::future` by value.
   * \deprecated
   */
  [[deprecated("FutureAndRequestId: use .future instead of an implicit conversion")]]
  operator FutureT() {return this->future;}

  // delegate future like methods in the std::future impl_

  /// See std::future::get().
  auto get() {return this->future.get();}
  /// See std::future::valid().
  bool valid() const noexcept {return this->future.valid();}
  /// See std::future::wait().
  void wait() const {return this->future.wait();}
  /// See std::future::wait_for().
  template<class Rep, class Period>
  std::future_status wait_for(
    const std::chrono::duration<Rep, Period> & timeout_duration) const
  {
    return this->future.wait_for(timeout_duration);
  }
  /// See std::future::wait_until().
  template<class Clock, class Duration>
  std::future_status wait_until(
    const std::chrono::time_point<Clock, Duration> & timeout_time) const
  {
    return this->future.wait_until(timeout_time);
  }

  // Rule of five, we could use the rule of zero here, but better be explicit as some of the
  // methods are deleted.

  /// Move constructor.
  FutureAndRequestId(FutureAndRequestId && other) noexcept = default;
  /// Deleted copy constructor, each instance is a unique owner of the future.
  FutureAndRequestId(const FutureAndRequestId & other) = delete;
  /// Move assignment.
  FutureAndRequestId & operator=(FutureAndRequestId && other) noexcept = default;
  /// Deleted copy assignment, each instance is a unique owner of the future.
  FutureAndRequestId & operator=(const FutureAndRequestId & other) = delete;
  /// Destructor.
  ~FutureAndRequestId() = default;
};
}  // namespace detail
detail::FutureAndRequestId

1,结构体模板;

使用时需指定FutureT的类型,async_send_request默认使用std::future<SharedResponse>类型;

1,两个重要的成员:

  FutureT future; //请求返回的结果,也就是响应
  int64_t request_id; //这个请求的id

Future

template<typename FutureT>
struct FutureAndRequestId
{
  FutureT future;
  int64_t request_id;

  .......

}

struct FutureAndRequestId
    : detail::FutureAndRequestId<std::future<SharedResponse>>{}

Future指定为std::future<SharedResponse>

也就是Future的类型时固定的std::future类型,而std::future的模板参数是接收的返回值(响应对象)的类型;

using SharedResponse = typename ServiceT::Response::SharedPtr;

ServiceT是建立客户端对象时使用的消息接口类型,也就是结构体类型;

eg:

rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr

ServiceT=example_interfaces::srv::AddTwoInts;

所以完整的Future的类型时:

std::future<example_interfaces::srv::AddTwoInts::Response::SharedPtr> Future

Future.get()

就是std::future中的模板参数类型的值:

std::future<SharedResponse>

std::future<ServiceT::Response::SharedPtr>

Future.get()的结果类型=ServiceT::Response::SharedPtr

eg:

std::future<example_interfaces::srv::AddTwoInts::Response::SharedPtr>

Future.get()的结果类型=example_interfaces::srv::AddTwoInts::Response::SharedPtr

#include <chrono>

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

using namespace std::chrono_literals;

class ServiceClient_01:public rclcpp::Node{
public:
    ServiceClient_01(std::string name):Node(name){
        RCLCPP_INFO(this->get_logger(),"I am service client 01: %s",name.c_str());
        client_=this->create_client<example_interfaces::srv::AddTwoInts>("add_tow_ints_srv");
    }

    auto sendRequest(std::shared_ptr<example_interfaces::srv::AddTwoInts_Request> request){
        //1,等待server上线
        while(!client_->wait_for_service(1s)){
            //failed,检查rclcpp状态
            if(!rclcpp::ok()){
                RCLCPP_INFO(this->get_logger(),"interrupted while waiting for service.");
                break;
            }
            RCLCPP_INFO(this->get_logger(),"Wainting for the server to work...");
        }
        //2,构造请求
        // int n1=0,n2=0;
        // std::cin>>n1>>n2;
        // auto request=std::make_shared<example_interfaces::srv::AddTwoInts_Request>();
        // request->a=n1;
        // request->b=n2;
        //3,发送请求
        auto result=client_->async_send_request(request);
        return result;
    }

    // void responseCallback(example_interfaces::srv::AddTwoInts::Response::SharedPtr result){
    //     //auto response=result_future.get();
    //     RCLCPP_INFO(this->get_logger(),"Caculate result: %ld",result->sum);
    // }

    void responseCallback(std::future<example_interfaces::srv::AddTwoInts::Response::SharedPtr> result_future){
        auto response=result_future.get();
        RCLCPP_INFO(this->get_logger(),"Caculate result: %ld",response->sum);
    }
    

private:
    rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_{};

    //回调函数
    // void _responseCallback(rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture result_future){
    //     auto response=result_future.get();
    //     RCLCPP_INFO(this->get_logger(),"Caculate result: %ld",response->sum);
    // }
};

int main(int argc,char** argv){
    rclcpp::init(argc,argv);
    auto node=std::make_shared<ServiceClient_01>("service_client_01");
    auto request=std::make_shared<example_interfaces::srv::AddTwoInts_Request>();
    std::cin>>request->a>>request->b;
    auto result=node->sendRequest(request);
    //rclcpp::spin(node);
    if(rclcpp::spin_until_future_complete(node,result)==rclcpp::FutureReturnCode::SUCCESS){
        //node->responseCallback(result.future.get());
        node->responseCallback(std::move(result.future));
    }
    rclcpp::shutdown();
    return 0;
}

异步的意义

在 ROS 2 中,异步操作指的是不会阻塞主线程的操作。当调用异步函数时,该函数会立即返回,而不会等待操作完成。相反,它会继续执行后续代码,因此可以同时进行多个操作,而不会阻塞程序的执行。

在 `async_send_request()` 中,发送请求的操作是异步进行的。它会在后台发送请求,而不会阻塞主线程的执行。在请求的响应到达时,会触发回调函数,该函数会在主线程上执行。这种方法允许程序同时进行多个请求,在等待响应时不会阻塞主线程的执行。

回调函数能够被触发执行的前提条件是主线程不能阻塞,如果主线程因为其他原因阻塞了,那么即使内核触发回调函数,主线程也无法执行回调函数。

eg:sleep(),std::cin>>a等;

回调函数

client端回调函数只需一个参数,用于获取响应对象,从响应对象中获取响应数据;

rclcpp::Client<serviceT>::SharedFuture 

SharedFuture是一个类对象,不是一个指针;

eg:

rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture result_future

服务端节点创建服务端对象

对象类型

rclcpp::Service<serviceT>::SharedPtr

模板参数serviceT是一个消息接口。

本质上是一个结构体;

这个结构体中有这个接口对应的请求对象和响应对象成员;

eg:

rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr

create_service

至少要传递前两个参数;

需指定模板参数---服务接口类型;

回调函数

回调函数有两个共享指针参数,一个请求对象参数,一个响应对象参数。直接从这两个对象中获取请求数据,处理之后,将数据放入响应对象,即可完成数据处理和响应。

这两个共享指针的参数是create_service时指定的服务的类型对应的类型:

eg:

create_service<example_interfaces::srv::AddTwoInts>()

回调函数:

void callback(std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)()

服务为什么不需要创建队列大小

服务为什么不想话题一样,创建发起对象和接收对象时,指定接收队列的大小?

服务端需要接收队列吗?

参数--parameter

什么是参数

参数本质上是节点中的一个类对象;

因为参数是通过节点的接口声明和设置的。

Parameter类

class Parameter
{
public:
  /// Construct with an empty name and a parameter value of type rclcpp::PARAMETER_NOT_SET.
  RCLCPP_PUBLIC
  Parameter();

  /// Construct with given name and a parameter value of type rclcpp::PARAMETER_NOT_SET.
  RCLCPP_PUBLIC
  explicit Parameter(const std::string & name);

  /// Construct with given name and given parameter value.
  RCLCPP_PUBLIC
  Parameter(const std::string & name, const ParameterValue & value);

  /// Construct with given name and given parameter value.
  template<typename ValueTypeT>
  Parameter(const std::string & name, ValueTypeT value)
  : Parameter(name, ParameterValue(value))
  {}
  ......
private:
  std::string name_;
  ParameterValue value_;
};

Parameter中有两个成员:参数的名称,参数的值;

ParameterValue也是一个类,而且有很多构造函数,可以指定任意类型的参数的值。

作用

在ros2中会存在一些需要通过CLI实时设置的参数,在ros2中使用参数的功能就是为了可以根据需要实时地设置参数的值。

参数的组成

参数名+参数值

参数的接口

declare_parameter

声明和初始化一个参数

template<typename ParameterT>
auto
Node::declare_parameter(
  const std::string & name,
  const ParameterT & default_value,
  const rcl_interfaces::msg::ParameterDescriptor & parameter_descriptor,
  bool ignore_override)
{
  try {
    return this->declare_parameter(
      name,
      rclcpp::ParameterValue(default_value),
      parameter_descriptor,
      ignore_override
    ).get<ParameterT>();
  } catch (const ParameterTypeException & ex) {
    throw exceptions::InvalidParameterTypeException(name, ex.what());
  }
}

只需传递前面两个值。

describe_parameter(name)

通过参数名字获取参数的描述

 get_parameter

通过参数名字获取一个参数的值

template<typename ParameterT>
bool
Node::get_parameter(const std::string & name, ParameterT & parameter) const
{
  std::string sub_name = extend_name_with_sub_namespace(name, this->get_sub_namespace());

  rclcpp::Parameter parameter_variant;

  bool result = get_parameter(sub_name, parameter_variant);
  if (result) {
    parameter = static_cast<ParameterT>(parameter_variant.get_value<ParameterT>());
  }

  return result;
}

 set_parameter

使用Parameter类对象为节点已经声明的参数设置值。

RCLCPP_PUBLIC

rcl_interfaces::msg::SetParametersResult

set_parameter(const rclcpp::Parameter & parameter);

注意:设置的参数必须已经声明;

set_parameters

使用Parameter类对象数组为节点已经声明的多个参数设置值。

RCLCPP_PUBLIC

std::vector<rcl_interfaces::msg::SetParametersResult>

set_parameters(const std::vector[rclcpp::Parameter](rclcpp::Parameter) & parameters);

注意:设置的参数必须已经声明;

has_parameter()

判断是否存在这个参数。

bool has_paramater(std::string& name)

参数值的设置方式

1,使用接口设置参数的值;

this->set_parameter(rclcpp::Parameter("param1",1))

2,启动节点是设置参数的值;

ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10

1,2两种同时存在,以1为有效;

3,运行时设置参数的值;

#查看参数列表
ros2 param list 
#设置参数级别
ros2 param set /parameters_basic rcl_log_level 10

参数命令

查看当前运行的所有节点的参数列表:

ros2 param list

查看参数描述:

ros2 param describe /turtlesim background_b

获取参数的值:

ros2 param get /turtlesim background_b

设置参数的值:

ros2 param set <node_name> <parameter_name> <value>

参数保存为快照:

小鱼教程

动作--action

概念

action是ros中完成数据通信的四种方式之一,用于完成.action类型的接口的数据通信。

作用

话题:单向传输---无反馈;

服务:请求---等待一次反馈--反馈--结束;

话题:发送请求--多次实时反馈--最后返回最终结果。

rclcpp_action包

官网

这个包的作用

提供了action编程所需要的接口,对象和枚举等数据;

动作客户端实现过程

1,建立客户端对象

rclcpp_action::Client<MoveRobot>::SharedPtr client_ptr_;

this->client_ptr_ = rclcpp_action::create_client<MoveRobot>(this, "move_robot");

2,绑定三个回调函数

目标响应回调函数;

eg:

using GoalHandleMoveRobot = rclcpp_action::ClientGoalHandle<MoveRobot>;

void goal_response_callback(GoalHandleMoveRobot::SharedPtr goal_handle) {}

作用:

用于检查服务端对请求目标是否可执行的判断结果;

过程反馈函数;

void feedback_callback( GoalHandleMoveRobot::SharedPtr, const std::shared_ptr<const MoveRobot::Feedback> feedback) {}

结果反馈函数;

void result_callback(const GoalHandleMoveRobot::WrappedResult& result) {}

反馈的结果对象是什么:

struct WrappedResult

{

/// The unique identifier of the goal

GoalUUID goal_id;  

/// A status to indicate if the goal was canceled, aborted, or suceeded

ResultCode code;

/// User defined fields sent back with an action

typename ActionT::Result::SharedPtr result;

};

code的种类:

enum class ResultCode : int8_t

{

UNKNOWN = action_msgs::msg::GoalStatus::STATUS_UNKNOWN,

SUCCEEDED = action_msgs::msg::GoalStatus::STATUS_SUCCEEDED,

CANCELED = action_msgs::msg::GoalStatus::STATUS_CANCELED,

ABORTED = action_msgs::msg::GoalStatus::STATUS_ABORTED

};

3,发送目标

4步:

void send_goal(){

......

//1,建立发送的消息对象

auto goal_msg = MoveRobot::Goal();

goal_msg.distance = 10;

RCLCPP_INFO(this->get_logger(), "Sending goal");

//2,建立发送目标选项对象

auto send_goal_options =rclcpp_action::Client<MoveRobot>::SendGoalOptions();

//3,设置回调函数

send_goal_options.goal_response_callback =std::bind(&RobotControl01::_goal_response_callback, this, _1);

send_goal_options.feedback_callback =std::bind(&RobotControl01::_feedback_callback, this, _1, _2);

send_goal_options.result_callback =std::bind(&RobotControl01::_result_callback, this, 1);

//4,发送目标

_ action_client_->async_send_goal(goal_msg, send_goal_options);

......

}

动作服务端实现过程

建立服务端对象绑定三个回调函数即可

类型:

rclcpp_action::Server<actionT>::SharedPtr 

接口--create_server:
template<typename ActionT>
typename Server<ActionT>::SharedPtr
create_server(
  rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node_base_interface,
  rclcpp::node_interfaces::NodeClockInterface::SharedPtr node_clock_interface,
  rclcpp::node_interfaces::NodeLoggingInterface::SharedPtr node_logging_interface,
  rclcpp::node_interfaces::NodeWaitablesInterface::SharedPtr node_waitables_interface,
  const std::string & name,
  typename Server<ActionT>::GoalCallback handle_goal,
  typename Server<ActionT>::CancelCallback handle_cancel,
  typename Server<ActionT>::AcceptedCallback handle_accepted,
  const rcl_action_server_options_t & options = rcl_action_server_get_default_options(),
  rclcpp::CallbackGroup::SharedPtr group = nullptr)
{}

三个回调函数:
handle_goal:
作用:

用于监测客户端的请求是否可执行,并对监测反馈给客户端。

参数:

第一个是:

rclcpp_action::GoalUUID& uuid,

一个请求(对象)的唯一标识符。

本质是一个静态数组:

#define UUID_SIZE 16

using GoalUUID = std::array<uint8_t, UUID_SIZE>;

存储16个8位的整数;16字节的数据。

为什么使用一个数组来存储标识符?

第二个:

std::shared_ptr<const MoveRobot::Goal> goal;

目标结构体的共享指针。

handle_cancel:

目标被服务端执行之后,客户端进行请求取消继续执行目标请求的回调函数;

参数:

std::shared_ptr<rclcpp_action::ServerGoalHandle<actionT>> goal_handle

目标请求的句柄对象。

注意:不是单个句柄值,而是一个对象。

handle_accepted:

接受目标请求并开始执行目标的回调函数。

第一个回调函数执行的结果为ACCEPT,就会执行第三个回调。

参数:

std::shared_ptr<rclcpp_action::ServerGoalHandle<actionT>> goal_handle

目标请求的句柄对象。

执行目标的回调函数内部,一般要开启新的线程执行目标任务并实时的反馈,防止阻塞主线程。

过程反馈的实现过程

使用话题的方式反馈数据:

auto feedback = std::make_shared<MoveRobot::Feedback>();

feedback->pose = robot.get_current_pose();

feedback->status = robot.get_status();

goal_handle->publish_feedback(feedback);

结果的反馈的实现过程

auto result = std::make_shared<MoveRobot::Result>();

if (goal_handle->is_canceling()) {

result->pose = robot.get_current_pose();

goal_handle->canceled(result); //通过这个接口返回结果

RCLCPP_INFO(this->get_logger(), "Goal Canceled");

return;

}

内部原理:

 接口

什么是接口

这里的接口不是编程语言中的函数接口。而是应该理解为在ros2中进行数据通信的接口;这些接口在ros2中使用时必须有统一的标准,就像陷淖USB接口一样有着统一的通信协议。所以这里的接口更好的理解是:ros2数据通信的统一接口。

我们创建话题的时候,需要指定话题的类型,创建服务的时候也需要指定客户端和服务端服务的类型,不管是话题还是服务,这些类型都是结构体。

而这些通信的对象,不管是发起端还是接收端,都要指定数据通信的接口类型,而且只有发起端和接收端的接口类型一致,才能进行正常的数据通信。

接口的作用

指定数据通信两端使用的统一协议(统一数据格式--本质上就是统一结构体类型)。

ros2的四种通信方式和3中接口

链接

ros2原生的接口

查看ros2中所有接口

ros2 interface list

包括ros2原生的,以及后续添加的。

查看某个接口的定义

ros2 interface show 接口的完整路径(功能包/目录/接口名)

接口文件定义

msg,srv,action文件中都可以定义哪些类型的变量?

小鱼教程

1,9个基础数据类型;

2,自定义类型;

自定义接口

1,创建功能包及接口类别目录

在功能包目录下创建msg(话题),或者srv(服务),或者action(动作)功能包;

2,创建接口文件

在msg目录下创建xxx.msg话题接口文件,在srv目录下创建xxx.srv服务文件,在action目录下创建xxx.action动作文件;

3,定义接口文件

话题接口--msg

话题是单项传输,所以只需要描述一次传输的数据有哪些即可;

一个msg中的所有变量就是一次传输的所有数据。

int32 x

int32 y

一次需要传输的消息对象的数据有这些成员。

服务接口--srv

服务需要包含请求和应答数据;

srv文件中需要定义请求和应答两个部分的数据,这两个部分需要以三个横线分割;

#请求数据

int64 a int64 b

---

#响应的数据

int64 sum

动作接口--action

动作接口文件需要三个部分组成:

目标+结果+之间状态反馈;

中间还是以三个横线分割;

# Goal: 要移动的距离
float32 distance
---
# Result: 最终的位置
float32 pose
---
# Feedback: 中间反馈的位置和状态
float32 pose
uint32 status
uint32 STATUS_MOVEING = 3
uint32 STATUS_STOP = 4

4,cmake增加接口路径

(1)添加生成接口的cmake接口库:

find_package(rosidl_default_generators REQUIRED)

(2)添加接口文件中依赖的其他接口库:

eg:

uint32 STATUS_MOVEING = 1
uint32 STATUS_STOP = 2
uint32  status
geometry_msgs/Pose pose

cmake:

find_package(geometry_msgs REQUIRED)

(3)利用cmake的接口将指定的接口文件生成接口

rosidl_generate_interfaces(${PROJECT_NAME} "msg/RobotPose.msg" "msg/RobotStatus.msg" "srv/MoveRobot.srv" DEPENDENCIES geometry_msgs )

注意:rosidl_generate_interface后有s.

5,package声明依赖

需要声明哪些依赖:

(1)接口文件需要依赖的包:

<depend>rosidl_default_generators</depend>

(2)声明这个包所属的组:

<member_of_group>rosidl_interface_packages</member_of_group>

表示该ROS2包是roidl_interface_packages组的成员,该组用于标识包含ROS2消息和服务定义的包。这样可以方便地将包分类和组织起来。

6,colcon构建生成接口文件

colcon build --package-select 功能包名

生成的文件位于:

工作空间的install/功能包/includ/功能包/功能包  下。

生成的接口的本质

colcon构建生成接口之后,使用:

ros2 interface package 功能包名称

可以查看功能包中定义的接口。

msg--结构体定义

使用以下案例来说明接口的定义和生成:

功能包和接口文件:

robot_move_interface/msg/RobotPose.msg

文件名:

RobotPose.msg

文件内容:

uint32 STATUS_MOVING=1

uint32 STATUS_STOP=2

uint32 status

geometry_msgs/Pose pose

concol构建:

colcon build --package-select robot_move_interface

构建之后会在工作目录下的install下生成robot_move_interface功能包,功能包中的include目录下会生成接口文件对应的c++代码文件。也就是构建之后,cmake 将我们在msg文件中对接口内容的描述生成了对应的c++代码文件。

重点:

(1)接口头文件和源文件格式。

colcon生成的c++接口头文件和源文件会以我们定义msg,srv,action文件时大写字母为分割,使用下划线连接,生成接口文件。

eg:

msg:

RobotPose.msg

c++:

robot_pose.h

robot_pose.hpp

(2)生成的内容

msg目录和srv目录下主要有一个detail目录和一些头文件,这些头文件中就包含我们使用时需要引用的头文件,比如:

robot_pose.hpp

(3)被引用头文件内容:

// generated from rosidl_generator_cpp/resource/idl.hpp.em
// generated code does not contain a copyright notice

#ifndef ROBOT_MOVE_INTERFACE__MSG__ROBOT_POSE_HPP_
#define ROBOT_MOVE_INTERFACE__MSG__ROBOT_POSE_HPP_

#include "robot_move_interface/msg/detail/robot_pose__struct.hpp"
#include "robot_move_interface/msg/detail/robot_pose__builder.hpp"
#include "robot_move_interface/msg/detail/robot_pose__traits.hpp"

#endif  // ROBOT_MOVE_INTERFACE__MSG__ROBOT_POSE_HPP_

被引用的头文件只是简单的引用了detail目录中的三个头文件。

而detail目录下的三个头文件,我们在msg,srv,action中描述的内容转化为struct.hpp中的结构体类型。

(4)detail中的结构体文件。

文件内容:

namespace robot_move_interface

{

namespace msg

{

// message struct

template<class ContainerAllocator>

struct RobotPose_

{

......

// field types and members

using _status_type =

uint32_t;

_status_type status;

using pose_type =

_ geometry_msgs::msg::Pose_<ContainerAllocator>;

_pose_type pose;

// setters for named parameter idiom

Type & set__status(

const uint32_t & _arg)

{

this->status = arg;

return *this;

}

Type & set__pose(

_ const geometry_msgs::msg::Pose_<ContainerAllocator> & _arg)

{

this->pose = _arg;

return *this;

}

// constant declarations

static constexpr uint32_t STATUS_MOVING =

1u;

static constexpr uint32_t STATUS_STOP =

2u;

......

};

重点在于:

【1】以功能包/msg/msg文件名_的格式定义结构体;

注意:这里是以“消息文件的文件名+下划线”的方式生成结构体。

【2】结构体内定义我们描述的变量,常量,在构造函数内有对变量的初始化;

【3】对于变量有如同protobuf一样的set_变量名的设置函数;

我们最终使用的结构体不是:

robot_move_interface::msg::RobotPose_  

的格式,而是:

robot_move_interface::msg::RobotPose

这种没有下划线的格式。

所以在xxx_struct.hpp中有如下声明:

using RobotPose =

robot_move_interface::msg::RobotPose_<std::allocator<void>>;

这就是我们最终使用的结构体的声明。

srv--结构体定义

.srv描述:

#请求数据

float32 distance

---

#响应数据

float32 pose

.hpp中的结构体定义:

请求结构体:

namespace robot_move_interface
{

namespace srv
{

// message struct
template<class ContainerAllocator>
struct MoveRobot_Request_
{
  ......
  // field types and members
  using _distance_type =
    float;
  _distance_type distance;

  // setters for named parameter idiom
  Type & set__distance(
    const float & _arg)
  {
    this->distance = _arg;
    return *this;
  }
};  // struct MoveRobot_Request_
// alias to use template instance with default allocator
using MoveRobot_Request =
  robot_move_interface::srv::MoveRobot_Request_<std::allocator<void>>;
}
}

响应结构体:

namespace robot_move_interface
{

namespace srv
{

// message struct
template<class ContainerAllocator>
struct MoveRobot_Response_
{
  ......
  // field types and members
  using _pose_type =
    float;
  _pose_type pose;

  // setters for named parameter idiom
  Type & set__pose(
    const float & _arg)
  {
    this->pose = _arg;
    return *this;
  }
};  // struct MoveRobot_Response_

// alias to use template instance with default allocator
using MoveRobot_Response =
  robot_move_interface::srv::MoveRobot_Response_<std::allocator<void>>;

// constant definitions
}  // namespace srv
}  // namespace robot_move_interface

接口结构体:

namespace robot_move_interface
{

namespace srv
{

struct MoveRobot
{
  using Request = robot_move_interface::srv::MoveRobot_Request;
  using Response = robot_move_interface::srv::MoveRobot_Response;
};

}  // namespace srv

}  // namespace robot_move_interface

重要的就是接口中只有两个请求和响应结构体对象。

action--结构定义

action构建下会构建一个action目录和一个msg目录;

action目录下的detail:

(1)目标结构体定义:

namespace action_interface
{

namespace action
{

// message struct
template<class ContainerAllocator>
struct MoveRobot_Goal_
{
  ......
  // field types and members
  using _distance_type =
    float;
  _distance_type distance;

  // setters for named parameter idiom
  Type & set__distance(
    const float & _arg)
  {
    this->distance = _arg;
    return *this;
  }
  ......
};  // struct MoveRobot_Goal_

// alias to use template instance with default allocator
using MoveRobot_Goal =
  action_interface::action::MoveRobot_Goal_<std::allocator<void>>;

// constant definitions

}  // namespace action

}  // namespace action_interface

(2)结果结构体的定义:

namespace action_interface
{

namespace action
{

// message struct
template<class ContainerAllocator>
struct MoveRobot_Result_
{
  ......
  // field types and members
  using _pose_type =
    float;
  _pose_type pose;

  // setters for named parameter idiom
  Type & set__pose(
    const float & _arg)
  {
    this->pose = _arg;
    return *this;
  }
  ......
};  // struct MoveRobot_Result_

// alias to use template instance with default allocator
using MoveRobot_Result =
  action_interface::action::MoveRobot_Result_<std::allocator<void>>;

// constant definitions

}  // namespace action

}  // namespace action_interface

(3)反馈结构体的定义:

namespace action_interface
{

namespace action
{

// message struct
template<class ContainerAllocator>
struct MoveRobot_Feedback_
{
  ......
  // field types and members
  using _status_type =
    float;
  _status_type status;
  using _pose_type =
    uint32_t;
  _pose_type pose;

  // setters for named parameter idiom
  Type & set__status(
    const float & _arg)
  {
    this->status = _arg;
    return *this;
  }
  Type & set__pose(
    const uint32_t & _arg)
  {
    this->pose = _arg;
    return *this;
  }

  // constant declarations
  static constexpr uint32_t STATUS_MOVING =
    3u;
  static constexpr uint32_t STATUS_STOP =
    4u;
  ......
};  // struct MoveRobot_Feedback_

// alias to use template instance with default allocator
using MoveRobot_Feedback =
  action_interface::action::MoveRobot_Feedback_<std::allocator<void>>;
}  // namespace action

}  // namespace action_interface

太多了,看源码。

我们如何在c++文件中引用自定义功能包中的接口文件?

需要在CMakeLists.txt文件中find_package()找到我们自定义的功能包的配置文件,在通ament_target_dependencies()和项目建立链接关系。

find_package(robot_move_interface REQUIRED)

add_executable(robot_move src/robot_move_server.cpp)

ament_target_dependencies(robot_move rclcpp robot_move_interface)

这样就可以使用自定义的接口文件中我定义的接口(结构体)了。

接口的命令

查看功能包中定义了哪些接口

ros2 interface package 功能包名称

生成的接口包不能引用

如果没有配置shell脚本,生成的接口包在构建之后需要执行以下install中的脚本使新生成的脚本发挥作用。

ros2怎么实现在不同设备之间的通信

ROS2编程基础课程文档 ROS 2(机器人操作系统2)是用于机器人应用的开源开发套件。ROS 2之目的是为各行各业的开发人员提供标准的软件平台,从研究和原型设计再到部署和生产。 ROS 2建立在ROS 1的成功基础之上,ROS 1目前已在世界各地的无数机器人应用中得到应用。 特色 缩短上市时间 ROS 2提供了开发应用程序所需的机器人工具,库和功能,可以将时间花在对业务非常重要的工作上。因为它 是开源的,所以可以灵活地决定在何处以及如何使用ROS 2,以及根据实际的需求自由定制,使用ROS 2 可以大幅度提升产品和算法研发速度! 专为生产而设计 凭借在建立ROS 1作为机器人研发的事实上的全球标准方面的十年经验,ROS 2从一开始就被建立在工业级 基础上并可用于生产,包括高可靠性和安全关键系统。 ROS 2的设计选择、开发实践和项目管理基于行业利 益相关者的要求。 多平台支持 ROS 2在Linux,Windows和macOS上得到支持和测试,允许无缝开发和部署机器人自动化,后端管理和 用户界面。分层支持模型允许端口到新平台,例如实时和嵌入式操作系统,以便在获得兴趣和投资时引入和推 广。 丰富的应用领域 与之前的ROS 1一样,ROS 2可用于各种机器人应用,从室内到室外、从家庭到汽车、水下到太空、从消费 到工业。 没有供应商锁定 ROS 2建立在一个抽象层上,使机器人库和应用程序与通信技术隔离开来。抽象底层是通信代码的多种实现, 包括开源和专有解决方案。在抽象顶层,核心库和用户应用程序是可移植的。 建立在开放标准之上 ROS 2中的默认通信方法使用IDL、DDS和DDS-I RTPS等行业标准,这些标准已广泛应用于从工厂到航空 航天的各种工业应用中。 开源许可证 ROS 2代码在Apache 2.0许可下获得许可,在3条款(或“新”)BSD许可下使用移植的ROS 1代码。这两个 许可证允许允许使用软件,而不会影响用户的知识产权。 全球社区 超过10年的ROS项目通过发展一个由数十万开发人员和用户组成的全球社区,为机器人技术创建了一个庞大 的生态系统,他们为这些软件做出贡献并进行了改进。 ROS 2由该社区开发并为该社区开发,他们将成为未 来的管理者。 行业支持 正如ROS 2技术指导委员会成员所证明的那样,对ROS 2的行业支持很强。除了开发顶级产品外,来自世界 各地的大大小小公司都在投入资源为ROS 2做出开源贡献。 与ROS1的互操作性 ROS 2包括到ROS 1的桥接器,处理两个系统之间的双向通信。如果有一个现有的ROS 1应用程序, 可 以通过桥接器开始尝试使用ROS 2,并根据要求和可用资源逐步移植应用程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿饵块

有帮助就给点辛苦费吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值