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的对应关系
发布者和订阅者都需要在节点中创建使用。
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中的脚本使新生成的脚本发挥作用。