ROS2(c++)

概念:

节点:

ROS中的每个节点应负责单个模块用途(例如,一个节点用于控制车轮电机,一个用于控制激光测距仪等)。每个节点都可以通过主题、服务、操作或参数向其他节点发送和接收数据。

启动一个节点:

ros2 run <package_name> <executable_name>

一些有关节点的指令:

ros2 node list//查看节点列表(常用)
ros2 node info <node_name>//查看节点信息(常用)
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle//重映射节点名称
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10//运行节点时设置参数

工作空间:

工作空间可以类比于VS中的SolutionDir,是一个目录,包含build、install、log、src四个子文件夹,src保存源码。

功能包:

功能包可以理解为存节点的地方,在ROS2中根据编译方式的不同,可以分为:

    ament_python,适用于python程序
    cmake,适用于C++
    ament_cmake,适用于C++程序,是cmake的增强版

-----------------------------------------------------------------------------------------

操作

创建一个ROS 2工作空间的步骤如下:

  1. 创建一个新的目录用作工作空间。
  2. 创建一个src子目录。
  3. 初始化工作空间和ROS 2包。

以下是相应的命令行操作:

# 创建工作空间目录
mkdir -p ros2_workspace/src

# 初始化新的ROS 2包,例如包名为my_robot
cd ros2_workspace/src
ros2 pkg create my_robot  --build-type  {cmake,ament_cmake,ament_python}  --dependencies <依赖名字>

# 编译工作空间
cd ros2_workspace
colcon build

# 源工作空间设置脚本
source install/local_setup.bash

一些指令

create       Create a new ROS2 package
executables  Output a list of package specific executables
list         Output a list of available packages
prefix       Output the prefix path of a package
xml          Output the XML of the package manifest or a specific tag

实例1:

打印helloworld。

mkdir -p ros2/src
cd ros2/src
ros2 pkg create pkg01_helloworld_cpp --build-type  ament_cmake --dependencies rclcpp --node-name helloworld

此处的--node-name 是创建了一个hellolworld的可执行程序

cd ..//回到工作空间下

colcon build
. install/setup.bash //刷新一下
ros2 run pkg01_helloworld_cpp helloworld

完成。

下面将进行节点的创建:

1.在vscode中打开工作空间,在工作空间的src下创建一个功能包

2.在功能包的src下创建一个文件名.cpp, 即创建了节点文件

下面我们将编写节点文件。

1.导入库文件

2.初始化客户端库

3.新建节点对象

4.spin循环节点

5.关闭客户端

    // 1. 导入库文件
# include "rclcpp/rclcpp.hpp"

int main(int argc,char **argv)
{
    //2.初始化客户端库
    rclcpp::init(argc,argv);
    //3.使用智能指针创建新的节点对象
    auto node = std::make_shared<rclcpp::Node>("ros_1");// 当我们初始化节点的时候要给他一个参数,给节点起一个名字

    // ros的打印
    RCLCPP_INFO(node->get_logger(),"打印内容");

    //4.使用spin循环节点
    rclcpp::spin(node);
    //5. 关闭客户端库
    rclcpp::shutdown();
}

注释:auto node是一个智能的类型,可以是任何形式,int char string.....

spin函数是一个用于节点(Node)的重要函数,它允许节点进入事件循环,以处理ROS 2中的消息和事件。以下是关于spin函数的解释:

    事件循环:事件循环是一个节点的核心机制,它使节点能够接收、处理和发布ROS 2中的消息、服务请求和其他事件。ROS 2中的事件循环类似于传统的消息循环或事件循环,它允许节点等待和响应来自其他节点的消息和事件。

    spin函数的作用:spin函数是ROS 2节点的主要事件循环函数之一。当你调用节点的spin函数时,节点将进入一个无限循环,等待并处理来自ROS 2通信系统的消息和事件。这包括订阅的消息、服务请求、参数更改等。

    消息处理:在事件循环中,节点可以在消息到达时执行相应的回调函数。例如,如果节点订阅了一个话题,当新消息发布到该话题时,节点的回调函数将被调用以处理这些消息。这使得节点能够实时地响应和处理外部事件。

    非阻塞:spin函数是非阻塞的,这意味着节点在等待事件发生时不会阻止程序的其他部分运行。节点可以并行处理其他任务,而不必等待消息的到达。

rclcpp实际上就是一个函数库

shared_ptr:可以指向特定类型的对象,用于自动释放所指的对象

make_shared:功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr

RCLCPP_INFO:用类似于printf()函数的方式输出日志,这样--->RCLCPP_INFO(A,"%d%c",变量1,变量2);那个A表示什么我还不知道。我觉得应该是日志的地址指针,this->get_logger();

完成上述操作后还需要到cmakelist.txt下进行下一步操作

打开cmakelist.txt后加入下面两句代码:

add_executable(node_01 src/node_01.cpp)
ament_target_dependencies(node_01 rclcpp)

第一句是添加可执行文件,第二句是添加依赖。

再加两句:

install(TARGETS
  node_01
  DESTINATION lib/${PROJECT_NAME}
)

这两句的作用是将节点放到install目录下,然后在source的时候可以找到

疑难: 一些新的函数例如makeshare,init,spin....需要去深度地了解其背后的使用方法

话题与服务

ROS中的话题(Topic),就是Node之间信息传输的方式。
Topic是一对多的传输方式。即一个话题的发布者,可以由多个订阅者来订阅。
比如,机器人现在左转,现在有一个名为”方向”的发布者,向所有订阅者发布“左转”这条信息。方向控制器作为一个订阅者,收到消息后,就控制系统左转;云台作为一个订阅者,得知消息后,也同时控制摄像头往左转,观测前方是否有障碍。

简单来说就是 :节点之间通过话题来实现信息的交流

话题通信的原则有以下:

1.发布订阅接口要相同,发布的是字符,订阅也需要用字符接口来接收

2.同一个节点可以订阅多个话题,也可以发布发布多个话题

3.同一个话题可以有多个发布者

实验一下

创建一个订阅者;

流程:

1.导入话题的接口类型

2.创建订阅回调函数(理解为:执行回调函数是为了让这个订阅者节点能够一直对发布者响应)

3.声明并创立订阅者(此处有点难理解)

4.编写订阅回调处理逻辑

完整代码如下:

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


using std::placeholders::_1;
using std::placeholders::_2;

/*
    创建一个类节点,名字叫做SingleDogNode,继承自Node.
*/
class SingleDogNode : public rclcpp::Node
{

public:
    // 构造函数,有一个参数为节点名称
    SingleDogNode(std::string name) : Node(name)
    {
        // 打印一句自我介绍
        RCLCPP_INFO(this->get_logger(), "大家好,我是单身狗%s.", name.c_str());
         // 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
        sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));
    }

private:
    // 声明一个订阅者(成员变量),用于订阅小说
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;

    // 收到话题数据的回调函数
    void topic_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        RCLCPP_INFO(this->get_logger(), "朕已阅:'%s'", msg->data.c_str());
    };

};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    /*产生一个Wang2的节点*/
    auto node = std::make_shared<SingleDogNode>("wang2");
    /* 运行节点,并检测退出信号*/
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

代码个人理解:

    // 收到话题数据的回调函数
    void topic_callback(const std_msgs::msg::String::SharedPtr msg)
    {
 
    };

这段是回调函数的实现,就是当订阅者收到来自发布者的消息后,对其进行反馈。

里面是对其反馈的内容,topic_callback是一个函数,传入的是一个共享指针名为msg

----------------------------------------------------------------------------------------------------------------------

// 声明一个订阅者(成员变量),用于订阅小说
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;

 声明是对订阅者进行定义,rclcpp::Subscription<>是一个模板类,里面传入的是订阅话题的类型,所以我们订阅的话题类型是std_msgs::msg::String。其实<>里面传入的通常是类,也就是说string也是一个类。

// 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
        sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));

reate_subscription<>是一个创建一个订阅者并且返回的函数,<>里面传入接口类型,()里面传入第一个是话题名字,第二个是队列长度(未了解),第三个是接收到话题的信息后,将信息传递的地方。我们在此处是想要把消息传递给回调函数进行处理,因为不能直接在第三个参数处写topic_callback,所以此处使用了bind函数,bind函数传入的第一个参数是回调函数,第二个参数是这个类的对象,也就是现在这个SingleDogNode类的对象,第三个参数是占位符,对应的数量与回调函数的参数数量一致。

msg->data.c_str()是将话题中的数据转化成c语言的string类型。

--------------------------------------------------------------------------------------------------------------------------

创建一个发布者:

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


using std::placeholders::_1;
using std::placeholders::_2;

/*
    创建一个类节点,名字叫做SingleDogNode,继承自Node.
*/
class SingleDogNode : public rclcpp::Node
{

public:
    // 构造函数,有一个参数为节点名称
    SingleDogNode(std::string name) : Node(name)
    {
        // 打印一句自我介绍
        RCLCPP_INFO(this->get_logger(), "大家好,我是单身狗%s.", name.c_str());
         // 创建一个订阅者来订阅李四写的小说,通过名字sexy_girl
        sub_novel = this->create_subscription<std_msgs::msg::String>("sexy_girl", 10, std::bind(&SingleDogNode::topic_callback, this, _1));
        // 创建发布者
        pub_money = this->create_publisher<std_msgs::msg::UInt32>("sexy_girl_money", 10);
    }

private:
    // 声明一个订阅者(成员变量),用于订阅小说
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_novel;

    // 声明一个发布者(成员变量),用于给李四钱
    rclcpp::Publisher<std_msgs::msg::UInt32>::SharedPtr pub_money;

    // 收到话题数据的回调函数
    void topic_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        // 新建一张人民币
        std_msgs::msg::UInt32 money;
        money.data = 10;
        // 发送人民币给李四
        pub_money->publish(money);

        RCLCPP_INFO(this->get_logger(), "朕已阅:'%s',打赏李四:%d 元的稿费", msg->data.c_str(), money.data);
    };

};

上述发布者的代码与订阅的代码非常相似,大体的函数都差不多。需要注意的是

   // 新建一张人民币
        std_msgs::msg::UInt32 money;
        money.data = 10;
        // 发送人民币给李四
        pub_money->publish(money);

这串代码将money定义了一下,然后通过publish函数将其发布出去,因为他是在回调函数中,所以,也就是将money发布给了一开始的发布者,也就是李四。

接口:

接口可以抹平不同编码语言对数据类型的差异

所以什么是接口,它是一种相互关系,只有彼此匹配,才能建立连接。

接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。

为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的,比如这里看到的int32表示32位的整型数,int64表示64位的整型数,bool表示布尔值,还可以定义数组、结构体,这些定义在编译过程中,会自动生成对应到C++、Python等语言里的数据结构。

ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。

  • 话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值。
  • 服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“—”区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数a和b,应答是求和的结果sum。
  • 动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是动作的目标,比如是开始运动,运动的结果,最终旋转的90度是否完成,还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。

自定义接口,为了满足个性化的需求,往往需要我们自己去定义一个接口。

过程:

在工作空间下的src下创建一个新的接口功能包;

进入接口功能包中新建一个msg文件,在msg文件下新建一个Novel.msg文件(首字母大写)

在Novel.msg文件中编写如下:

# 标准消息接口std_msgs下的String类型
std_msgs/String content
# 图像消息,调用sensor_msgs下的Image类型
sensor_msgs/Image image

然后配置camkelist.txt文件,为了将Novel.msg转换成c++,python的头文件

在camkelist.txt中加入:

#添加对sensor_msgs的
find_package(sensor_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
#添加消息文件和依赖
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Novel.msg"
   DEPENDENCIES sensor_msgs
 )

 理解:

第一句的作用是告诉依赖了sensor_msgs要找到这个所在的包;

第二句是找到rosidl_generate_interfaces函数所在的包;该函数就是创建了一个接口;

--------------------------------------------------------------------------------------------------------------------------

服务

服务是什么

ROS程序由多个功能组成;一个功能由多个节点完成;各节点的功能独立,节点与节点间通过不同的通信方式,实现数据的传输,完成功能。即总功能由分功能组成,各分功能由节点的子功能及节点间的通信完成。
服务是一种节点与节点间进行一对一、双向传输的数据通信方式,数据通过服务的请求与响应实现传送。

由客户端向服务端提出请求,然后服务端对客户端进行响应

服务接口与话题借口不一样,因为话题多是单向的,而服务是双向的,要用新的接口格式

创建自定义服务接口:

  • 新建srv文件夹,并在文件夹下新建xxx.srv
  • xxx.srv下编写服务接口内容并保存
  • CmakeLists.txt添加依赖和srv文件目录
  • package.xml中添加xxx.srv所需的依赖(如果没有加入新的依赖则不需要这步)
  • 编译功能包即可生成python与c++头文件

 按照如下格式写进xxx.srv,---上的是请求的数据类型,下面的的是返回的数据类型。

int64 a
int64 b
---
int64 sum

 编写服务端实例:

在李三算计着借钱吃麻辣烫的时候,隔壁王家村王二也在和张家村张三讨价还价着。

原来隔壁张家村穷光蛋张三,想看艳娘传奇,但是买不起正版的,想从王二这边买王二看过的二手书。王二思虑一番后,答应了张三,但他有下面的两点要求:

  1. 必须一手交钱,一手交货,爱买不买
  2. 每次给多少钱卖多少章,每章一块钱,如果手里的存货不足,就继续等待

张三一看,虽然一块钱对我来说还是很贵,但想想美腻的艳娘,必须买。

实现步骤:

  1. 导入服务接口(int money,string[]novels)

  2. 创建服务端回调函数

  3. 声明并创建服务端

  4. 编写回调函数逻辑处理请求

 导入服务接口时要到package.xml下添加依赖,也就是服务端口的依赖。打开package.xml输入:

  <depend>village_interfaces</depend>

另外还需要到cmakelist.txt下加入 :(进行配置)

find_package(village_interfaces REQUIRED)

这一步是进行查找包,接下来还需要将其和可执行文件链接起来:

找到ament_target_dependencies改成如下

ament_target_dependencies(wang2_node 
  rclcpp 
  village_interfaces
)

完成这步后即可导入服务接口:

#include "village_interfaces/srv/sell_novel.hpp"

 

声明回调函数:

// 声明一个回调函数,当收到买书请求时调用该函数,用于处理数据
void sell_book_callback(const village_interfaces::srv::SellNovel::Request::SharedPtr request,
        const village_interfaces::srv::SellNovel::Response::SharedPtr response)
{
}

 理解:该函数传入了一个请求和回答指针,也就是说,当我们的客户端传入了请求的时候,我们也就要回调给客户端一个响应,在此处买书案例中,就是指买书的人提出了请求,那么我们的服务端的人就要使用回调函数处理这个请求,以及给予客户答复,如此一来便完成了回调函数。不理解的地方:为什么参数中有Request,和Response,他们是从哪里来的......

类中声明服务端:

 // 声明一个服务端
    rclcpp::Service<village_interfaces::srv::SellNovel>::SharedPtr server_;

 service<>也是一个模板类,里面的是接口类型。

然后需要一个库来放置旧书,要使用queue,来存放。

加入头文件#include <queue>;然后在类中的私有部分加入如下代码,创建了一个novels_queue库;

std::queue<std::string>  novels_queue;

squeue<>中的是传入容器中的数据的类型,此处是传的string,容器命名为noves_squeue;

然后此后出现了死锁,原因是我们在卖书的时候要查看容器中的书够不够,才能进行销售,然而同时地,正在等待卖书环节结束后,我们书的数量才能增加,所以,如果当容器中的书数量不够时,我们的卖书环节无法结束,在这一环节中等待书数量的增多,我们增加书的环节也在等待卖书的环节结束。那么这个互相等待的情况就叫死锁。

解决死锁问题需要用到多线程,一边卖书一边增加旧书的数量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值