古月·ROS2入门21讲——学习笔记(一)核心概念部分1-14讲

news2024/11/15 10:11:09

讲解视频地址:1.ROS和ROS2是什么_哔哩哔哩_bilibili

笔记分为上篇核心概念部分和下篇常用工具部分

下篇:古月·ROS2入门21讲——学习笔记(二)常用工具部分15-21讲-CSDN博客

目录

第一讲:ROS/ROS2是什么

1. ROS的诞生

2. ROS的发展

3. ROS的特点

4. ROS的社区

5. 21讲的结构

​编辑

第二讲:ROS2对比ROS1

1. ROS1的局限性

2. 全新的ROS2

3. ROS2 vs ROS1

3.1 系统架构

3.2 DDS通信

3.3 核心概念

第三讲:ROS2安装方法

安装教程:

第四讲:ROS2命令行操作

1. Linux的常用命令

2. ROS2中的命令行

 2.1 运行节点程序

 2.2 查看节点信息

 2.3 查看话题信息

2.4 发布话题消息

2.5 发送服务请求

 2.6 发送动作目标

2.7  录制控制命令

 第五讲:ROS2开发环境配置

第六讲:工作空间与功能包

1. 工作空间是什么

1.1 创建工作空间

1.2 自动安装依赖

1.3 编译工作空间 

1.4 设置环境变量

2. 功能包

2.1 创建功能包

2.2 编译功能包

2.3 功能包的结构

2.3.1 C++功能包

2.3.2 Python功能包

第七讲:节点

1. 通信模型

2. 案例一:Hello World节点(面向过程)

3. 创建节点流程

4. 案例二:Hello World节点(面向对象)

5. 案例三:物体识别节点

6. 案例四:机器视觉识别节点

第八讲:话题

1. 通信模型

2. 案例一:Hello World话题通信

3. 案例二:机器视觉识别

4. 话题命令行操作

第九讲:服务

1. 通信模型

2. 案例一:加法求解器

3. 案例二:机器视觉识别

4. 服务命令行操作

 第十讲:通信接口:数据传递的标准结构

1. 接口的定义

2. ROS通信接口

3. 语言无关

4. 案例一:服务接口的定义与使用

5. 案例二:话题接口的定义与使用

运行效果

接口定义

订阅者接口调用

6. 接口命令行操作

第十一讲:动作

1. 通信模型

客户端/服务器模型

一对多通信

同步通信

由服务和话题合成

2. 案例一:小海龟的动作

3. 案例二:机器人画圆

接口定义

通信模型

服务端代码解析

客户端代码解析

第十二讲:参数

1. 通信模型

全局字典

可动态监控

2. 案例一:小海龟例程中的参数

查看参数列表

参数查询与修改

参数文件保存与加载

运行效果

代码解析

3. 案例三:机器视觉应用

运行效果

代码解析

第十三讲:分布式通信

1. 分布式通信

2. 分布式网络搭建

装系统

安装ROS2

编译代码

远程桌面

3. 分布式数据传输

4. 分布式网络分组

第十四讲:DDS

1. 通信模型

什么是DDS

DDS在ROS2中的应用

质量服务策略QoS

2. 案例一:在命令行中配置DDS

3. 案例二:DDS编程示例

运行效果

发布者代码解析

订阅者代码解析


第一讲:ROS/ROS2是什么

1. ROS的诞生

对于越来越复杂的智能机器人系统,已经不是一个人或者一个团队可以独立完成的,如何高效开发机器人,是技术层面上非常重要的一个问题,针对这个问题,一群斯坦福大学的有志青年尝试给出一个答案,那就是机器人操作系统。 

我们可以看到PR2机器人已经可以完成叠毛巾、熨烫衣服、打台球、剪头发等一系列复杂的应用功能,以叠毛巾为例,这在当时是轰动机器人圈的重要研究,因为第一次有机器人可以完成柔性物体的处理,虽然效率很低,在100分钟之内只完成了5条毛巾的整理,但是在学术层面却推动机器人向前走了一大步。

这款机器人中的软件框架就是ROS的原型,所以ROS因这款个人服务机器人而生,很快也从中独立出来,成为一款用于更多机器人的软件系统。

2. ROS的发展

ROS诞生于2007年的斯坦福大学,这是早期PR2机器人的原型,这个项目很快被一家商业公司Willow Garage看中,类似现在的风险投资一样,他们投了一大笔钱给这群年轻人,PR2机器人在资本的助推下成功诞生。

2010年,随着PR2机器人的发布,其中的软件正式确定了名称,就叫做机器人操作系统,Robot Operating System,简称为ROS。同年,ROS也肩负着让更多人使用的使命,正式开源。

PR2机器人虽好,但是成本居高不下,几百万的价格让绝大部分开发者望而却步,官方也注意到了这个问题,所以在2011年发布了一款后期成为ROS圈爆款的机器人——Turtlebot,这款机器人采用扫地机器人的底盘,加上xbox游戏机中的体感传感器Kinect,直接使用笔记本电脑就可以控制,支持ROS的所有开源功能,关键是价格便宜,随着这款机器人的普及,大大推动了ROS的应用。

从2012年开始,使用ROS的人越来越多,ROS官方也开始每年举办一届ROS开发者大会——ROS Conference,简称ROSCon,来自全球的开发者会齐聚一堂分享自己使用ROS开发的机器人应用,其中不乏亚马逊、Intel、微软等大公司的身影,近两年因为疫情原因改为线上举办,名称也变为了ROS World。

经历前几年野蛮而快速的增长,ROS逐渐迭代稳定,2014年起,ROS跟随Ubuntu系统,每两年推出一个长期支持版,每个版本支持五年时间,这标志着ROS的成熟,也让ROS加快了普及的步伐。

回到时间轴的起点,ROS的创始团队原本只想做一款个人服务机器人,万万没想到,ROS被越来越多机器人使用,受限于当初设计的局限性,ROS的问题也逐渐暴露。为了能够真正设计一款适用于所有机器人的操作系统,ROS2在2017年底正式发布,历经多年迭代,我们终于在2022年5月底,迎来了ROS2第一个长期支持版——ROS2 Humble,ROS2已经成熟,我们也进入了一个全新的ROS2时代。

从ROS发展的时间轴中,我们不仅可以了解到ROS的发展过程,更重要的是熟悉ROS和ROS2诞生的原因。

这里我们也把ROS2发展的时间轴单独提取出来,介于ROS在各种各样机器人中应用的问题,ROS2在2014年提出,2015年开始迭代,2017年推出第一个正式版,此后快速迭代,直到2022年推出了第一个长期支持版。

3. ROS的特点

ROS以“提高机器人软件复用率”为目标,这个目标简单来讲就是不要重新造轮子。

一个目标
  • 提高机器人研发中的软件复用率
五个特点
  • 点对点的设计
  • 多语言支持
  • 架构精简、集成度高
  • 组件化工具包丰富
  • 免费并且开源
四位一体
  • ROS=通讯机制+开发工具+应用功能+生态系统

4. ROS的社区

ROS全球社区有几个重要网站:

  1. answers.ros.org,这是一个ROS问答网站,大家可以在上边提出任何关于ROS的问题,全球很多开发者都很乐意回答我们的问题;
  2. wiki.ros.org,这是ROS的维基百科,记录了ROS教程和各种功能包的使用;
  3. discourse.ros.org,这是ROS论坛,关于ROS开发的新鲜事都可以在这里发表和查看,比如ROS的活动、新功能包的发布等等。
  4. index.ros.org,是ROS各种资源的一个索引网站;
  5. packages.ros.org,是ROS功能包存储的数据库。



5. 21讲的结构

课程分为4部分

第二讲:ROS2对比ROS1

1. ROS1的局限性

ROS最早的设计目标就是开发这样一款PR2家庭服务机器人,这款机器人绝大部分时间都是独立工作,为了让他具备充足的能力:

  • 它搭载了工作站级别的计算平台和各种先进的通信设备,不用担忧算力不够,有足够的实力支持各种复杂的实时运算和处理;
  • 由于是单兵作战,通信绝大部分都自己内部完成,那就可以用有线连接,保证了良好的网络连接,没有丢数据或者黑客入侵的风险;
  • 这台机器人最终虽然小批量生产,但是由于高昂的成本和售价,也只能用于学术研究。

随着ROS的普及,应用ROS的机器人类型已经和PR2机器人有了天翻地覆的问题,也并不具备PR2这样的条件,那原本针对PR2设计的软件框架,就会出现一些问题,比如:

  • 要在资源有限的嵌入式系统中运行;
  • 要在有干扰的地方保证通信的可靠性;
  • 要做成产品走向市场,甚至用在自动驾驶汽车和航天机器人上。

类似的问题不断涌现,致使更加适合各种机器人应用的新一代ROS诞生了,也就是我们课程的主角——ROS2。

2. 全新的ROS2

 在设计之初,就考虑到要满足各种各样机器人应用的需求。

  1. 多机器人系统:未来机器人一定不会是独立的个体,机器人和机器人之间也需要通信和协作,ROS2为多机器人系统的应用提供了标准方法和通信机制。
  2. 跨平台:机器人应用场景不同,使用的控制平台也会有很大差异,比如自动驾驶汽车中的算力性能肯定比AMR机器人强很多,为了让所有机器人都可以运行ROS2,ROS2可以跨平台运行于Linux、Windows、MacOS、RTOS,甚至是没有任何系统的微控制器(MCU)上,这样我们就不用纠结自己的控制器能不能用ROS了。
  3. 实时性:机器人运动控制和很多行为策略要求机器人具备实时性,比如机器人要可靠得在100ms内发现前方的行人,或者稳定的在1ms周期内完成运动学、动力学的解算,ROS2为类似这样的实时性需求提供了基本保障。
  4. 网络连接:无论在怎样的网络环境下,ROS2都可以尽量保障机器人大量数据的完整性和安全性,比如在wifi信号不好的时候数据也要尽力发送过去,在有黑客入侵风险的场景下要对数据进行加密解密。
  5. 产品化:,大量机器人已经走向我们的生活,未来还会越来越多,ROS2不仅可以用于机器人研发阶段,还可以直接搭载在产品中,走向消费市场,这对ROS2的稳定性、强壮性也提除了巨大挑战。
  6. 项目管理:机器人开发是一个复杂的系统工程,设计、开发、调试、测试、部署等全流程的项目管理工具和机制,也会在ROS2中体现,更方便我们去开发一款机器人。

与ROS1相比,ROS2对系统架构和软件代码全部进行了重新设计和实现,体现在以下几点:

  • 系统架构进行了颠覆性的变化,ROS1中所有节点都需要在节点管理器ROS Master的管理下进行工作,一旦Master出现问题,系统就面临宕机的风险,ROS2实现了真正的分布式,不再有Master这个角色,借助一种全新的通信框架DDS,为所有节点的通信提供可靠保障。
  • 软件API进行了重新设计,ROS1原有的接口已经无法满足需求,ROS2结合C++最新标准和Python3语言特性,设计了更具通用性的API,虽然导致原有ROS1的代码无法直接在ROS2中运行,但是尽量保留了类似的使用方法,同时提供了大量移植的说明。
  • 编译系统进行了升级,ROS1中使用的rosbuild和catkin问题诸多,尤其是针对代码较多的大项目以及Python编写的项目,编译、链接经常会出错,ROS2对这些问题也进行了优化,重新优化后的编译系统叫做ament和colcon,大家在后续的课程中就会体验到新版编译器的使用方法。

这几点只是框架层面

3. ROS2 vs ROS1

3.1 系统架构

在这张图中,左侧是ROS1,右侧是ROS2,大家注意看两者最明显的变化,那就是Master

  • 在ROS1中,应用层里Master这个节点管理器的角色至关重要,所有节点都得听它指挥,类似是一个公司的CEO,有且只有一个,如果这个CEO突然消失,公司肯定会成一团乱麻。ROS2把这个最不稳定的角色请走了,节点可以通过另外一套discovery——自发现机制,找到彼此,从而建立稳定的通信连接。
  • 中间层是ROS封装好的标准通信接口,我们写程序的时候,会频繁和这些通信接口打交道,比如发布一个图像的数据,接收一个雷达的信息,客户端库会再调用底层复杂的驱动和通信协议,让我们的开发变得更加简单明了。
  • 在ROS1中,ROS通信依赖底层的TCP和UDP协议,而在ROS2中,通信协议更换成了更加复杂但也更加完善的DDS系统
  • 如果是在进程内需要进行大量数据的通信,ROS1和ROS2都提供了基于共享内存的通信方法,只不过名字不太一样而已。
  • 最下边是系统层,也就是可以将ROS安装在哪些操作系统上,ROS1主要安装在Linux上,ROS2的可选项就很多了,Linux、windows、MacOS、RTOS都可以。

3.2 DDS通信

ROS2相比ROS1最大的变化,除了省略了Master之外,应该就是通信系统的变化了。ROS1中基于TCP/UDP的通信系统,频繁诟病于延迟、丢数据、无法加密等问题,ROS2中的DDS在通信层面的功能就丰富多了。

DDS其实是物联网中广泛应用的一种通信协议,类似于我们常听说的5G通信一样,DDS是一个国际标准,能够实现该标准的软件系统并不是唯一的,所以我们可以选择多个厂家提供的DDS系统,比如这里的OpenSplice、FastRTPS,还有更多厂家提供的,每一家的性能不同,适用的场景也不同。

不过这就带来一个问题,每个DDS厂家的软件接口肯定是不一样的,如果我们按照某一家的接口写完了程序,想要切换其他厂家的DDS,不是要重新写代码么?这当然不符合ROS提高软件复用率的目标。

为了解决这个问题,ROS2设计了一个ROS Middleware,简称RMW,也就是指定一个标准的接口,比如如何发数据,如何收数据,数据的各种属性如何配置,都定义好了,如果厂家想要接入ROS社区,就得按照这个标准写一个适配的接口,把自家的DDS给移植过来,这样就把问题交给了最熟悉自家DDS的厂商。对于我们这些用户来讲,某一个DDS用的不爽,只要安装另一个,然后做一个简单的配置,程序一行的都不用改,轻松更换底层的通信系统。

举一个例子,比如我们在产品开发时,可以先用开源版本的DDS满足基本需求,部署交付的产品时,再更换为商业版本更稳定的DDS,这样可以减少开发成本。

总之,DDS的加入,让ROS2系统更加稳定,也更加灵活,当然复杂度也会高一些。这样,我们不用再纠结ROS的通信系统是否稳定、该如何优化等问题,更多精力都可以放在其他三个部分,专注优化我们的机器人应用功能。

3.3 核心概念

大家已经熟悉了ROS1的开发方式以及其中的很多概念,ROS2尽量保留了这些概念。

第三讲:ROS2安装方法

安装教程:

ROS2安装方法 - ROS2入门教程 (guyuehome.com)

我这里也是用虚拟机安装了ubuntu22.04来安装ros2的,我的双系统是ubuntu20.04且安装了ros1,不知道什么原因并不能安装ros2,才用了虚拟机。

虚拟机安装只有这一个问题,下面是解决办法

解决raw.githubusercontent无法访问(仅限终端获取文件) - 知乎 (zhihu.com)

第四讲:ROS2命令行操作

1. Linux的常用命令

cd

  • 语法:cd <目录路径>

  • 功能:改变工作目录。若没有指定“目录路径”,则回到用户的主目录

pwd

  • 语法:pwd
  • 功能:此命令显示出当前工作目录的绝对路径

mkdir

  • 语法:mkdir [选项] <目录名称>

  • 功能:创建一个目录/文件夹

ls

  • 语法:ls [选项] [目录名称…]

  • 功能:列出目录/文件夹中的文件列表

 gedit

  • 语法:gedit <文件名称>

  • 功能:打开gedit编辑器编辑文件,若没有此文件则会新建

mv

  • 语法:mv [选项] <源文件或目录> <目地文件或目录>

  • 功能:为文件或目录改名或将文件由一个目录移入另一个目录中

cp

  • 语法:cp [选项] <源文件名称或目录名称> <目的文件名称或目录名称>

  • 功能:把一个文件或目录拷贝到另一文件或目录中,或者把多个源文件复制到目标目录中

 rm

  • 语法:rm [选项] <文件名称或目录名称…>

  • 功能:该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除。对于链接文件,只是删除了链接,原有文件均保持不变

sudo

  • 语法:sudo [选项] [指令]

  • 功能:以系统管理员权限来执行指令

2. ROS2中的命令行

ROS2命令行的操作机制与Linux相同,不过所有操作都集成在一个ros2的总命令中,后边第一个参数表示不同的操作目的,比如node表示对节点的操作,topic表示对话题的操作,具体操作干什么,还可以在后边继续跟一系列参数内容。

 2.1 运行节点程序

想要运行ROS2中某个节点,我们可以使用ros2 run命令进行操作,例如我们要运行海龟仿真节点和键盘控制节点:

ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key

 2.2 查看节点信息

当前运行的ROS系统中都有哪些节点呢?可以这样来查看:

ros2 node list

 如果对某一个节点感兴趣,加上一个info子命令,就可以知道它的详细信息啦:

ros2 node info /turtlesim 

 2.3 查看话题信息

当前系统中都有话题呢,使用如下命令即可查看:

ros2 topic list

 还想看到某一个话题中的消息数据,加上echo子命令试一试:

ros2 topic echo /turtle1/pose 

2.4 发布话题消息

想要控制海龟动起来,我们还可以直接通过命令行发布话题指令:

ros2 topic pub --rate 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"

2.5 发送服务请求

一只海龟太孤单,仿真器还提供改了一个服务——产生海龟,我们试一试服务调用,再来一只海龟:

ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: ''}"

 2.6 发送动作目标

想要让海龟完成一个具体动作,比如转到指定角度,仿真器中提供的这个action可以帮上忙,通过命令行这样发送动作目标:

ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "theta: 3"

2.7  录制控制命令

系统运行中的数据有很多,如果想要把某段数据录制下来,回到实验室再复现这段数据如何?ROS2中的rosbag功能还是很好用的,轻松实现数据的录制与播放:

ros2 bag record /turtle1/cmd_vel
ros2 bag play rosbag2_2022_04_11-17_35_40/rosbag2_2022_04_11-17_35_40_0.db3

 第五讲:ROS2开发环境配置

Linux中安装git

sudo apt install git
git clone https://gitee.com/guyuehome/ros2_21_tutorials.git

VSCode

官方网站:Visual Studio Code - Code Editing. Redefined

VSCode插件配置 

  • Chinese
  • Python
  • C++
  • CMake
  • vscode-icons
  • ROS
  • Msg Language Support
  •  Visual Studio IntelliCode
  • URDF
  • Markdown All in One

第六讲:工作空间与功能包

1. 工作空间是什么

类似的,在ROS机器人开发中,我们针对机器人某些功能进行代码开始时,各种编写的代码、参数、脚本等文件,也需要放置在某一个文件夹里进行管理,这个文件夹在ROS系统中就叫做工作空间

ROS系统中一个典型的工作空间结构如图所示,这个dev_ws就是工作空间的根目录,里边会有四个子目录,或者叫做四个子空间。

  • src,代码空间,未来编写的代码、脚本,都需要人为的放置到这里;
  • build,编译空间,保存编译过程中产生的中间文件;
  • install,安装空间,放置编译得到的可执行文件和脚本;
  • log,日志空间,编译和运行过程中,保存各种警告、错误、信息等日志。

这里也要强调一点,工作空间的名称我们可以自己定义,数量也并不是唯一的。

1.1 创建工作空间

使用如下命令创建一个工作空间,并且下载教程的代码:

$ mkdir -p ~/dev_ws/src
$ cd ~/dev_ws/src
$ git clone https://gitee.com/guyuehome/ros2_21_tutorials.git

1.2 自动安装依赖

$ sudo apt install -y python3-pip
$ sudo pip3 install rosdepc
$ sudo rosdepc init
$ rosdepc update
$ cd ..
$ rosdepc install -i --from-path src --rosdistro humble -y

1.3 编译工作空间 

依赖安装完成后,就可以使用如下命令编译工作空间啦,如果有缺少的依赖,或者代码有错误,编译过程中会有报错,否则编译过程应该不会出现任何错误:

$ sudo apt install python3-colcon-ros
$ cd ~/dev_ws/
$ colcon build

编译成功后,就可以在工作空间中看到自动生产的build、log、install文件夹了。

1.4 设置环境变量

$ source install/local_setup.sh # 仅在当前终端生效
$ echo " source ~/dev_ws/install/local_setup.sh" >> ~/.bashrc # 所有终端均生效

2. 功能包

功能包原理:我们把不同功能的代码划分到不同的功能包中,尽量降低他们之间的耦合关系,当需要在ROS社区中分享给别人的时候,只需要说明这个功能包该如何使用,别人很快就可以用起来了。

2.1 创建功能包

$ ros2 pkg create --build-type <build-type> <package_name>

ros2命令中:

  • pkg:表示功能包相关的功能;
  • create:表示创建功能包;
  • build-type:表示新创建的功能包是C++还是Python的,如果使用C++或者C,那这里就跟ament_cmake,如果使用Python,就跟ament_python;
  • package_name:新建功能包的名字。

比如在终端中分别创建C++和Python版本的功能包:

$ cd ~/dev_ws/src
$ ros2 pkg create --build-type ament_cmake learning_pkg_c               # C++
$ ros2 pkg create --build-type ament_python learning_pkg_python # Python

2.2 编译功能包

在创建好的功能包中,我们可以继续完成代码的编写,之后需要编译和配置环境变量,才能正常运行:

$ cd ~/dev_ws
$ colcon build   # 编译工作空间所有功能包
$ source install/local_setup.bash

2.3 功能包的结构

功能包并不是普通的文件夹,那如何判断一个文件夹是否是功能包呢?我们来分析下刚才新创建两个功能包的结构。

2.3.1 C++功能包

首先看下C++类型的功能包,其中必然存在两个文件:package.xmlCMakerLists.txt

package.xml文件的主要内容,包含功能包的版权描述,和各种依赖的声明。

CMakeLists.txt文件是编译规则,C++代码需要编译才能运行,所以必须要在该文件中设置如何编译,使用CMake语法。

2.3.2 Python功能包

C++功能包需要将源码编译成可执行文件,但是Python语言是解析型的,不需要编译,所以会有一些不同,但也会有这两个文件:package.xmlsetup.py

 package.xml文件的主要内容和C++版本功能包一样,包含功能包的版权描述,和各种依赖的声明。

setup.py文件里边也包含一些版权信息,除此之外,还有“entry_points”配置的程序入口,在后续编程讲解中,我们会给大家介绍如何使用。

第七讲:节点

机器人是各种功能的综合体,每一项功能就像机器人的一个工作细胞,众多细胞通过一些机制连接到一起,成为了一个机器人整体。

在ROS中,我们给这些 “细胞”取了一个名字,那就是节点

1. 通信模型

完整的机器人系统可能并不是一个物理上的整体,比如这样一个的机器人:

在机器人身体里搭载了一台计算机A,它可以通过机器人的眼睛——摄像头,获取外界环境的信息,也可以控制机器人的腿——轮子,让机器人移动到想要去的地方。除此之外,可能还会有另外一台计算机B,放在你的桌子上,它可以远程监控机器人看到的信息,也可以远程配置机器人的速度和某些参数,还可以连接一个摇杆,人为控制机器人前后左右运动。

这些功能虽然位于不同的计算机中,但都是这款机器人的工作细胞,也就是节点,他们共同组成了一个完整的机器人系统。

  • 节点在机器人系统中的职责就是执行某些具体的任务,从计算机操作系统的角度来看,也叫做进程;
  • 每个节点都是一个可以独立运行的可执行文件,比如执行某一个python程序,或者执行C++编译生成的结果,都算是运行了一个节点;
  • 既然每个节点都是独立的执行文件,那自然就可以想到,得到这个执行文件的编程语言可以是不同的,比如C++、Python,乃至Java、Ruby等更多语言。
  • 这些节点是功能各不相同的细胞,根据系统设计的不同,可能位于计算机A,也可能位于计算机B,还有可能运行在云端,这叫做分布式,也就是可以分布在不同的硬件载体上;
  • 每一个节点都需要有唯一的命名,当我们想要去找到某一个节点的时候,或者想要查询某一个节点的状态时,可以通过节点的名称来做查询。

2. 案例一:Hello World节点(面向过程)

$ ros2 run learning_node node_helloworld

代码解析

learning_node/node_helloworld.py

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2节点示例-发布“Hello World”日志信息, 使用面向过程的实现方式
"""

import rclpy                                   # ROS2 Python接口库
from rclpy.node import Node                    # ROS2 节点类
import time

def main(args=None):                           # ROS2节点主入口main函数
    rclpy.init(args=args)                      # ROS2 Python接口初始化
    node = Node("node_helloworld")             # 创建ROS2节点对象并进行初始化

    while rclpy.ok():                          # ROS2系统是否正常运行
        node.get_logger().info("Hello World")  # ROS2日志输出
        time.sleep(0.5)                        # 休眠控制循环时间

    node.destroy_node()                        # 销毁节点对象    
    rclpy.shutdown()                           # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'node_helloworld       = learning_node.node_helloworld:main',
        ],

3. 创建节点流程

代码中出现的函数大家未来会经常用到,大家先不用纠结函数的具体使用方法,更重要的是理解节点的编码流程。

总结一下,想要实现一个节点,代码的实现流程是这样做:

  • 编程接口初始化
  • 创建节点并初始化
  • 实现节点功能
  • 销毁节点并关闭接口

大家如果有学习过C++或者Pyhton的话,应该可以发现这里我们使用的是面向过程的编程方法,这种方式虽然实现简单,但是对于稍微复杂一点的机器人系统,就很难做到模块化编码。

4. 案例二:Hello World节点(面向对象)

$ ros2 run learning_node node_helloworld_class

代码解析

learning_node/node_helloworld_class.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2节点示例-发布“Hello World”日志信息, 使用面向对象的实现方式
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
import time

"""
创建一个HelloWorld节点, 初始化时输出“hello world”日志
"""
class HelloWorldNode(Node):
    def __init__(self, name):
        super().__init__(name)                     # ROS2节点父类初始化
        while rclpy.ok():                          # ROS2系统是否正常运行
            self.get_logger().info("Hello World")  # ROS2日志输出
            time.sleep(0.5)                        # 休眠控制循环时间

def main(args=None):                               # ROS2节点主入口main函数
    rclpy.init(args=args)                          # ROS2 Python接口初始化
    node = HelloWorldNode("node_helloworld_class") # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                               # 循环等待ROS2退出
    node.destroy_node()                            # 销毁节点对象
    rclpy.shutdown()                               # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'node_helloworld       = learning_node.node_helloworld:main',
         'node_helloworld_class = learning_node.node_helloworld_class:main',
        ],

5. 案例三:物体识别节点

$ sudo apt install python3-opencv
$ ros2 run learning_node node_object    #注意修改图片路径后重新编译

运行前需要将learning_node/node_object.py代码中的图片路径,修改为实际路径,修改后重新编译运行即可:

image = cv2.imread('/home/hcx/dev_ws/src/ros2_21_tutorials/learning_node/learning_node/apple.jpg')

例程运行成功后,会弹出一个可视化窗口,可以看到苹果被成功识别啦,一个绿色框会把苹果的轮廓勾勒出来,中间的绿点表示中心点。

代码解析

learning_node/node_object.py

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2节点示例-通过颜色识别检测图片中出现的苹果
"""

import rclpy                            # ROS2 Python接口库
from rclpy.node import Node             # ROS2 节点类

import cv2                              # OpenCV图像处理库
import numpy as np                      # Python数值计算库

lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

def object_detect(image):
    hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)      # 图像从BGR颜色模型转换为HSV模型
    mask_red = cv2.inRange(hsv_img, lower_red, upper_red) # 图像二值化

    contours, hierarchy = cv2.findContours(mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) # 图像中轮廓检测

    for cnt in contours:                                  # 去除一些轮廓面积太小的噪声
        if cnt.shape[0] < 150:
            continue

        (x, y, w, h) = cv2.boundingRect(cnt)              # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
        cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)# 将苹果的轮廓勾勒出来
        cv2.circle(image, (int(x+w/2), int(y+h/2)), 5, (0, 255, 0), -1)# 将苹果的图像中心点画出来

    cv2.imshow("object", image)                           # 使用OpenCV显示处理后的图像效果
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def main(args=None):                                      # ROS2节点主入口main函数
    rclpy.init(args=args)                                 # ROS2 Python接口初始化
    node = Node("node_object")                            # 创建ROS2节点对象并进行初始化
    node.get_logger().info("ROS2节点示例:检测图片中的苹果")

    image = cv2.imread('/home/hcx/dev_ws/src/ros2_21_tutorials/learning_node/learning_node/apple.jpg')  # 读取图像
    object_detect(image)                                   # 苹果检测
    rclpy.spin(node)                                       # 循环等待ROS2退出
    node.destroy_node()                                    # 销毁节点对象
    rclpy.shutdown()                                       # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
    'console_scripts': [
     'node_helloworld       = learning_node.node_helloworld:main',
     'node_helloworld_class = learning_node.node_helloworld_class:main',
     'node_object           = learning_node.node_object:main',
    ],

6. 案例四:机器视觉识别节点

$ ros2 run learning_node node_object_webcam #注意设置摄像头

 如果是在虚拟机中操作,需要进行以下设置: 1. 把虚拟机设置为兼容USB3.1; 2. 在可移动设备中将摄像头连接至虚拟机。

运行成功后,该节点就可以驱动摄像头,并且实时识别摄像头中的红色物体啦。 

代码解析

相比之前的程序,这里最大的变化是修改了图片的来源,使用OpenCV中的VideoCapture()来驱动相机,并且周期read摄像头的信息,并进行识别。

learning_node/node_object_webcam.py

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2节点示例-通过摄像头识别检测图片中出现的苹果
"""

import rclpy                            # ROS2 Python接口库
from rclpy.node import Node             # ROS2 节点类

import cv2                              # OpenCV图像处理库
import numpy as np                      # Python数值计算库

lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

def object_detect(image):
    hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)       # 图像从BGR颜色模型转换为HSV模型
    mask_red = cv2.inRange(hsv_img, lower_red, upper_red)  # 图像二值化

    contours, hierarchy = cv2.findContours(mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) # 图像中轮廓检测

    for cnt in contours:                                   # 去除一些轮廓面积太小的噪声
        if cnt.shape[0] < 150:
            continue

        (x, y, w, h) = cv2.boundingRect(cnt)               # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
        cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2) # 将苹果的轮廓勾勒出来
        cv2.circle(image, (int(x+w/2), int(y+h/2)), 5, (0, 255, 0), -1)    # 将苹果的图像中心点画出来

    cv2.imshow("object", image)                            # 使用OpenCV显示处理后的图像效果
    cv2.waitKey(50)

def main(args=None):                                       # ROS2节点主入口main函数
    rclpy.init(args=args)                                  # ROS2 Python接口初始化
    node = Node("node_object_webcam")                      # 创建ROS2节点对象并进行初始化
    node.get_logger().info("ROS2节点示例:检测图片中的苹果")

    cap = cv2.VideoCapture(0)


    while rclpy.ok():
        ret, image = cap.read()          # 读取一帧图像

        if ret == True:
            object_detect(image)         # 苹果检测

    node.destroy_node()                  # 销毁节点对象
    rclpy.shutdown()                     # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
    'console_scripts': [
     'node_helloworld       = learning_node.node_helloworld:main',
     'node_helloworld_class = learning_node.node_helloworld_class:main',
     'node_object           = learning_node.node_object:main',
     'node_object_webcam    = learning_node.node_object_webcam:main',
    ],

第八讲:话题

1. 通信模型

  • 发布/订阅模型

从话题本身的实现角度来看,使用了基于DDS的发布/订阅模型,什么叫发布和订阅呢?

话题数据传输的特性是从一个节点到另外一个节点,发送数据的对象称之为发布者,接收数据的对象称之为订阅者,每一个话题都需要有一个名字,传输的数据也需要有固定的数据类型。

  • 多对多通信

因为话题是多对多的模型,发布控制指令的摇杆可以有一个,也可以有2个、3个,订阅控制指令的机器人可以有1个,也可以有2个、3个,大家可以想象一下这个画面,似乎还是挺魔性的,如果存在多个发送指令的节点,建议大家要注意区分优先级,不然机器人可能不知道该听谁的了。

  • 异步通信

话题通信还有一个特性,那就是异步,这个词可能有同学是第一次听说?所谓异步,只要是指发布者发出数据后,并不知道订阅者什么时候可以收到。

异步的特性也让话题更适合用于一些周期发布的数据,比如传感器的数据,运动控制的指令等等,如果某些逻辑性较强的指令,比如修改某一个参数,用话题传输就不太合适了。

  • 消息接口

最后,既然是数据传输,发布者和订阅者就得统一数据的描述格式,不能一个说英文,一个理解成了中文。在ROS中,话题通信数据的描述格式称之为消息,对应编程语言中数据结构的概念。比如这里的一个图像数据,就会包含图像的长宽像素值、每个像素的RGB等等,在ROS中都有标准定义。

消息是ROS中的一种接口定义方式,与编程语言无关,我们也可以通过.msg后缀的文件自行定义,有了这样的接口,各种节点就像积木块一样,通过各种各样的接口进行拼接,组成复杂的机器人系统。

2. 案例一:Hello World话题通信

从Hello World例程开始,我们来创建一个发布者,发布话题“chatter”,周期发送“Hello World”这个字符串,消息类型是ROS中标准定义的String,再创建一个订阅者,订阅“chatter”这个话题,从而接收到“Hello World”这个字符串。

启动第一个终端,运行话题的发布者节点:

$ ros2 run learning_topic topic_helloworld_pub

启动第二个终端,运行话题的发布者节点 2:

$ ros2 run learning_topic topic_helloLLL_pub

启动第三个终端,运行话题的订阅者节点:

$ ros2 run learning_topic topic_all_sub

可以看到发布者循环发布“Hello World” 和“Hello LLL”字符串消息,订阅者也以几乎同样的频率收到该话题的消息数据。

发布者代码解析

1. learning_topic/topic_helloworld_pub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布“Hello World”话题
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
from std_msgs.msg import String                  # 字符串消息类型

"""
创建一个发布者节点
"""
class PublisherNode(Node):

    def __init__(self, name):
        super().__init__(name)                                    # ROS2节点父类初始化
        self.pub = self.create_publisher(String, "chatter", 10)   # 创建发布者对象(消息类型、话题名、队列长度)
        self.timer = self.create_timer(0.5, self.timer_callback)  # 创建一个定时器(单位为秒的周期,定时执行的回调函数)

    def timer_callback(self):                                     # 创建定时器周期执行的回调函数
        msg = String()                                            # 创建一个String类型的消息对象
        msg.data = 'Hello World'                                  # 填充消息对象中的消息数据
        self.pub.publish(msg)                                     # 发布话题消息
        self.get_logger().info('Publishing: "%s"' % msg.data)     # 输出日志信息,提示已经完成话题发布

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = PublisherNode("topic_helloworld_pub")     # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

2. learning_topic/topic_helloLLL_pub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布“Hello World”话题
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node import Node                      # ROS2 节点类
from std_msgs.msg import String                  # 字符串消息类型

"""
创建一个发布者节点
"""
class PublisherNode(Node):
    
    def __init__(self, name):
        super().__init__(name)                                    # ROS2节点父类初始化
        self.pub = self.create_publisher(String, "chatter", 10)   # 创建发布者对象(消息类型、话题名、队列长度)
        self.timer = self.create_timer(0.5, self.timer_callback)  # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
        
    def timer_callback(self):                                     # 创建定时器周期执行的回调函数
        msg = String()                                            # 创建一个String类型的消息对象
        msg.data = 'Hello LLL'                                  # 填充消息对象中的消息数据
        self.pub.publish(msg)                                     # 发布话题消息
        self.get_logger().info('Publishing: "%s"' % msg.data)     # 输出日志信息,提示已经完成话题发布
        
def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = PublisherNode("topic_helloLLL_pub")     # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

流程总结

对以上程序进行分析,如果我们想要实现一个发布者,流程如下:

  • 编程接口初始化
  • 创建节点并初始化
  • 创建发布者对象
  • 创建并填充话题消息
  • 发布话题消息
  • 销毁节点并关闭接口

订阅者代码解析

程序实现

learning_topic/topic_all_sub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-同时订阅多个话题
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类
from std_msgs.msg import String                  # ROS2标准定义的String消息

"""
创建一个订阅者节点
"""
class SubscriberNode(Node):
    
    def __init__(self, name):
        super().__init__(name)                                    # ROS2节点父类初始化
        self.sub_chatter = self.create_subscription(\
            String, "chatter", self.listener_callback_chatter, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        self.sub_chatter1 = self.create_subscription(\
            String, "chatter1", self.listener_callback_chatter1, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)

    def listener_callback_chatter(self, msg):                      # chatter话题的回调函数
        self.get_logger().info('I heard chatter: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息
        
    def listener_callback_chatter1(self, msg):                     # chatter1话题的回调函数
        self.get_logger().info('I heard chatter1: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = SubscriberNode("topic_all_sub")    # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
        'console_scripts': [
         'topic_helloworld_pub  = learning_topic.topic_helloworld_pub:main',
         'topic_helloLLL_pub    = learning_topic.topic_helloLLL_pub:main',
         'topic_helloworld_sub  = learning_topic.topic_helloworld_sub:main',
         'topic_helloLLL_sub    = learning_topic.topic_helloLLL_sub:main',
         'topic_all_sub    = learning_topic.topic_all_sub:main',
        ],
    },

流程总结

对以上程序进行分析,如果我们想要实现一个订阅者,流程如下:

  • 编程接口初始化
  • 创建节点并初始化
  • 创建订阅者对象
  • 回调函数处理话题数据
  • 销毁节点并关闭接口

3. 案例二:机器视觉识别

在节点概念的讲解过程中,我们通过一个节点驱动了相机,并且实现了对红色物体的识别。功能虽然没问题,但是对于机器人开发来讲,并没有做到程序的模块化,更好的方式是将相机驱动和视觉识别做成两个节点,节点间的联系就是这个图像数据,通过话题周期传输即可。

$ ros2 run learning_topic topic_webcam_pub
$ ros2 run learning_topic topic_webcam_sub

将红色物体放入相机范围内,即可看到识别效果。

发布者代码解析

learning_topic/topic_webcam_pub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布图像话题
"""

import rclpy                        # ROS2 Python接口库
from rclpy.node import Node         # ROS2 节点类
from sensor_msgs.msg import Image   # 图像消息类型
from cv_bridge import CvBridge      # ROS与OpenCV图像转换类
import cv2                          # Opencv图像处理库

"""
创建一个发布者节点
"""
class ImagePublisher(Node):

    def __init__(self, name):
        super().__init__(name)                                           # ROS2节点父类初始化
        self.publisher_ = self.create_publisher(Image, 'image_raw', 10)  # 创建发布者对象(消息类型、话题名、队列长度)
        self.timer = self.create_timer(0.1, self.timer_callback)         # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
        self.cap = cv2.VideoCapture(0)                                   # 创建一个视频采集对象,驱动相机采集图像(相机设备号)
        self.cv_bridge = CvBridge()                                      # 创建一个图像转换对象,用于稍后将OpenCV的图像转换成ROS的图像消息

    def timer_callback(self):
        ret, frame = self.cap.read()                         # 一帧一帧读取图像

        if ret == True:                                      # 如果图像读取成功
            self.publisher_.publish(
                self.cv_bridge.cv2_to_imgmsg(frame, 'bgr8')) # 发布图像消息

        self.get_logger().info('Publishing video frame')     # 输出日志信息,提示已经完成图像话题发布

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = ImagePublisher("topic_webcam_pub")        # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

订阅者代码解析

learning_topic/topic_webcam_sub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-订阅图像话题
"""

import rclpy                            # ROS2 Python接口库
from rclpy.node import Node             # ROS2 节点类
from sensor_msgs.msg import Image       # 图像消息类型
from cv_bridge import CvBridge          # ROS与OpenCV图像转换类
import cv2                              # Opencv图像处理库
import numpy as np                      # Python数值计算库

lower_red = np.array([0, 90, 128])      # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])   # 红色的HSV阈值上限

"""
创建一个订阅者节点
"""
class ImageSubscriber(Node):
    def __init__(self, name):
        super().__init__(name)                                # ROS2节点父类初始化
        self.sub = self.create_subscription(
            Image, 'image_raw', self.listener_callback, 10)   # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        self.cv_bridge = CvBridge()                           # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

    def object_detect(self, image):
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)      # 图像从BGR颜色模型转换为HSV模型
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) # 图像二值化
        contours, hierarchy = cv2.findContours(
            mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)   # 图像中轮廓检测

        for cnt in contours:                                  # 去除一些轮廓面积太小的噪声
            if cnt.shape[0] < 150:
                continue

            (x, y, w, h) = cv2.boundingRect(cnt)              # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)# 将苹果的轮廓勾勒出来
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5,
                       (0, 255, 0), -1)                       # 将苹果的图像中心点画出来

        cv2.imshow("object", image)                           # 使用OpenCV显示处理后的图像效果
        cv2.waitKey(10)

    def listener_callback(self, data):
        self.get_logger().info('Receiving video frame')     # 输出日志信息,提示已进入回调函数
        image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8')  # 将ROS的图像消息转化成OpenCV图像
        self.object_detect(image)                           # 苹果检测


def main(args=None):                            # ROS2节点主入口main函数
    rclpy.init(args=args)                       # ROS2 Python接口初始化
    node = ImageSubscriber("topic_webcam_sub")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                            # 循环等待ROS2退出
    node.destroy_node()                         # 销毁节点对象
    rclpy.shutdown()                            # 关闭ROS2 Python接口

4. 话题命令行操作

话题命令的常用操作如下:

$ ros2 topic list                # 查看话题列表
$ ros2 topic info <topic_name>   # 查看话题信息
$ ros2 topic hz <topic_name>     # 查看话题发布频率
$ ros2 topic bw <topic_name>     # 查看话题传输带宽
$ ros2 topic echo <topic_name>   # 查看话题数据
$ ros2 topic pub <topic_name> <msg_type> <msg_data>   # 发布话题消息

第九讲:服务

1. 通信模型

在需要这个数据的时候,发一个查询的请求,然后尽快得到此时目标的最新位置。

这样的通信模型和话题单向传输有所不同,变成了发送一个请求,反馈一个应答的形式,好像是你问我答一样,这种通信机制在ROS中成为服务,Service

  • 客户端/服务器模型

从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。

这种通信机制在生活中也很常见,比如我们经常浏览的各种网页,此时你的电脑浏览器就是客户端,通过域名或者各种操作,向网站服务器发送请求,服务器收到之后返回需要展现的页面数据。

  • 同步通信 

这个过程一般要求越快越好,假设服务器半天没有反应,你的浏览器一直转圈圈,那有可能是服务器宕机了,或者是网络不好,所以相比话题通信,在服务通信中,客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信。

  • 一对多通信

所以服务通信模型中,服务器端唯一,但客户端可以不唯一。

  • 服务接口

和话题通信类似,服务通信的核心还是要传递数据,数据变成了两个部分,一个请求的数据,比如请求苹果位置的命令,还有一个反馈的数据,比如反馈苹果坐标位置的数据,这些数据和话题消息一样,在ROS中也是要标准定义的,话题使用.msg文件定义,服务使用的是.srv文件定义,后续我们会给大家介绍定义的方法。

2. 案例一:加法求解器

当我们需要计算两个加数的求和结果时,就通过客户端节点,将两个加数封装成请求数据,针对服务“add_two_ints”发送出去,提供这个服务的服务器端节点,收到请求数据后,开始进行加法计算,并将求和结果封装成应答数据,反馈给客户端,之后客户端就可以得到想要的结果啦。 

启动两个终端,并运行如下节点,第一个节点是服务端,等待请求数据并提供求和功能,第二个节点是客户端,发送传入的两个加数并等待求和结果。 

$ ros2 run learning_service service_adder_server
$ ros2 run learning_service service_adder_client 2 3
  • 客户端代码解析

learning_service/service_adder_client.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-发送两个加数,请求加法器计算
"""

import sys

import rclpy                                  # ROS2 Python接口库
from rclpy.node   import Node                 # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口

class adderClient(Node):
    def __init__(self, name):
        super().__init__(name)                                       # ROS2节点父类初始化
        self.client = self.create_client(AddTwoInts, 'add_two_ints') # 创建服务客户端对象(服务接口类型,服务名)
        while not self.client.wait_for_service(timeout_sec=1.0):     # 循环等待服务器端成功启动
            self.get_logger().info('service not available, waiting again...') 
        self.request = AddTwoInts.Request()                          # 创建服务请求的数据对象

    def send_request(self):                                          # 创建一个发送服务请求的函数
        self.request.a = int(sys.argv[1])
        self.request.b = int(sys.argv[2])
        self.future = self.client.call_async(self.request)           # 异步方式发送服务请求

def main(args=None):
    rclpy.init(args=args)                        # ROS2 Python接口初始化
    node = adderClient("service_adder_client")   # 创建ROS2节点对象并进行初始化
    node.send_request()                          # 发送服务请求

    while rclpy.ok():                            # ROS2系统正常运行
        rclpy.spin_once(node)                    # 循环执行一次节点

        if node.future.done():                   # 数据是否处理完成
            try:
                response = node.future.result()  # 接收服务器端的反馈数据
            except Exception as e:
                node.get_logger().info(
                    'Service call failed %r' % (e,))
            else:
                node.get_logger().info(          # 将收到的反馈信息打印输出
                    'Result of add_two_ints: for %d + %d = %d' % 
                    (node.request.a, node.request.b, response.sum))
            break

    node.destroy_node()                          # 销毁节点对象
    rclpy.shutdown()                             # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'service_adder_client  = learning_service.service_adder_client:main',
        ],
    },

流程总结

对以上程序进行分析,如果我们想要实现一个客户端,流程如下:

  • 编程接口初始化
  • 创建节点并初始化
  • 创建客户端对象
  • 创建并发送请求数据
  • 等待服务器端应答数据
  • 销毁节点并关闭接口

  • 服务端代码解析

至于服务器端的实现,有点类似话题通信中的订阅者,并不知道请求数据什么时间出现,也用到了回调函数机制。

程序实现

learning_service/service_adder_server.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供加法器的服务器处理功能
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类
from learning_interface.srv import AddTwoInts    # 自定义的服务接口

class adderServer(Node):
    def __init__(self, name):
        super().__init__(name)                                                           # ROS2节点父类初始化
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.adder_callback)  # 创建服务器对象(接口类型、服务名、服务器回调函数)

    def adder_callback(self, request, response):   # 创建回调函数,执行收到请求后对数据的处理
        response.sum = request.a + request.b       # 完成加法求和计算,将结果放到反馈的数据中
        self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))   # 输出日志信息,提示已经完成加法求和计算
        return response                          # 反馈应答信息

def main(args=None):                             # ROS2节点主入口main函数
    rclpy.init(args=args)                        # ROS2 Python接口初始化
    node = adderServer("service_adder_server")   # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                             # 循环等待ROS2退出
    node.destroy_node()                          # 销毁节点对象
    rclpy.shutdown()                             # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'service_adder_client  = learning_service.service_adder_client:main',
         'service_adder_server  = learning_service.service_adder_server:main',
        ],
    },

流程总结

对以上程序进行分析,如果我们想要实现一个服务端,流程如下:

  • 编程接口初始化
  • 创建节点并初始化
  • 创建服务器端对象
  • 通过回调函数处进行服务
  • 向客户端反馈应答结果
  • 销毁节点并关闭接口

3. 案例二:机器视觉识别

运行效果

此时会有三个节点出现:

  1. 相机驱动节点,发布图像数据;
  2. 视觉识别节点,订阅图像数据,并且集成了一个服务器端对象,随时准备提供目标位置;
  3. 客户端节点,我们可以认为是一个机器人目标跟踪的节点,当需要根据目标运动时,就发送一次请求,然后拿到一个当前的目标位置。

启动三个终端,分别运行上述三个节点:

$ ros2 run usb_cam usb_cam_node_exe
$ ros2 run learning_service service_object_server
$ ros2 run learning_service service_object_client
  • 客户端代码解析

learning_service/service_object_client.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-请求目标识别,等待目标位置应答
"""

import rclpy                                            # ROS2 Python接口库
from rclpy.node   import Node                           # ROS2 节点类
from learning_interface.srv import GetObjectPosition    # 自定义的服务接口

class objectClient(Node):
    def __init__(self, name):
        super().__init__(name)                          # ROS2节点父类初始化
        self.client = self.create_client(GetObjectPosition, 'get_target_position')
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.request = GetObjectPosition.Request()

    def send_request(self):
        self.request.get = True
        self.future = self.client.call_async(self.request)

def main(args=None):
    rclpy.init(args=args)                             # ROS2 Python接口初始化
    node = objectClient("service_object_client")      # 创建ROS2节点对象并进行初始化
    node.send_request()

    while rclpy.ok():
        rclpy.spin_once(node)

        if node.future.done():
            try:
                response = node.future.result()
            except Exception as e:
                node.get_logger().info(
                    'Service call failed %r' % (e,))
            else:
                node.get_logger().info(
                    'Result of object position:\n x: %d y: %d' %
                    (response.x, response.y))
            break
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'service_adder_client  = learning_service.service_adder_client:main',
         'service_adder_server  = learning_service.service_adder_server:main',
         'service_object_client = learning_service.service_object_client:main',
        ],
    },
  • 服务端代码解析

learning_service/service_object_server.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供目标识别服务
"""

import rclpy                                           # ROS2 Python接口库
from rclpy.node import Node                            # ROS2 节点类
from sensor_msgs.msg import Image                      # 图像消息类型
import numpy as np                                     # Python数值计算库
from cv_bridge import CvBridge                         # ROS与OpenCV图像转换类
import cv2                                             # Opencv图像处理库
from learning_interface.srv import GetObjectPosition   # 自定义的服务接口

lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

class ImageSubscriber(Node):
    def __init__(self, name):
        super().__init__(name)                              # ROS2节点父类初始化
        self.sub = self.create_subscription(
            Image, 'image_raw', self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        self.cv_bridge = CvBridge()                         # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

        self.srv = self.create_service(GetObjectPosition,   # 创建服务器对象(接口类型、服务名、服务器回调函数)
                                       'get_target_position',
                                       self.object_position_callback)    
        self.objectX = 0
        self.objectY = 0                              

    def object_detect(self, image):
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)      # 图像从BGR颜色模型转换为HSV模型
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) # 图像二值化
        contours, hierarchy = cv2.findContours(
            mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)   # 图像中轮廓检测

        for cnt in contours:                                  # 去除一些轮廓面积太小的噪声
            if cnt.shape[0] < 150:
                continue

            (x, y, w, h) = cv2.boundingRect(cnt)              # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)# 将苹果的轮廓勾勒出来
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5,
                       (0, 255, 0), -1)                       # 将苹果的图像中心点画出来

            self.objectX = int(x+w/2)
            self.objectY = int(y+h/2)

        cv2.imshow("object", image)                            # 使用OpenCV显示处理后的图像效果
        cv2.waitKey(50)

    def listener_callback(self, data):
        self.get_logger().info('Receiving video frame')        # 输出日志信息,提示已进入回调函数
        image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8')     # 将ROS的图像消息转化成OpenCV图像
        self.object_detect(image)                              # 苹果检测

    def object_position_callback(self, request, response):     # 创建回调函数,执行收到请求后对数据的处理
        if request.get == True:
            response.x = self.objectX                          # 目标物体的XY坐标
            response.y = self.objectY
            self.get_logger().info('Object position\nx: %d y: %d' %
                                   (response.x, response.y))   # 输出日志信息,提示已经反馈
        else:
            response.x = 0
            response.y = 0
            self.get_logger().info('Invalid command')          # 输出日志信息,提示已经反馈
        return response


def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = ImageSubscriber("service_object_server")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'service_adder_client  = learning_service.service_adder_client:main',
         'service_adder_server  = learning_service.service_adder_server:main',
         'service_object_client = learning_service.service_object_client:main',
         'service_object_server = learning_service.service_object_server:main',
        ],
    },

4. 服务命令行操作

$ ros2 service list                  # 查看服务列表
$ ros2 service type <service_name>   # 查看服务数据类型
$ ros2 service call <service_name> <service_type> <service_data>   # 发送服务请求

话题和服务是ROS中最为常用的两种数据通信方法,前者适合传感器、控制指令等周期性、单向传输的数据,后者适合一问一答,同步性要求更高的数据,比如获取机器视觉识别到的目标位置。

 第十讲:通信接口:数据传递的标准结构

1. 接口的定义

软件开发中,接口的使用就更多了,比如我们在编写程序时,使用的函数和函数的输入输出也称之为接口,每一次调用函数的时候,就像是把主程序和调用函数通过这个接口连接到一起,系统才能正常工作。

更为形象的是图形化编程中使用的程序模块,每一个模块都有固定的结构和形状,只有两个模块相互匹配,才能在一起工作,这就很好的讲代码形象化了。

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

2. ROS通信接口

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

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

3. 语言无关

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

  • 话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值。

  • 服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“---”区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数a和b,应答是求和的结果sum。

  • 动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是动作的目标,比如是开始运动,运动的结果,最终旋转的90度是否完成,还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。

4. 案例一:服务接口的定义与使用

有三个节点,第一个驱动相机发布图像话题,第二个是机器视觉识别节点,封装了一个服务的服务端对象,提供目标识别位置的查询服务,第三个节点在需要目标位置的时候,就可以发送请求,收到位置进行使用了。

  • 接口定义

在这个例程中,我们使用GetObjectPosition.srv定义了服务通信的接口:

learning_interface/srv/GetObjectPosition.srv

bool get      # 获取目标位置的指令
---
int32 x       # 目标的X坐标
int32 y       # 目标的Y坐标

定义中有两个部分,上边是获取目标位置的指令,get为true的话,就表示我们需要一次位置,服务端就会反馈这个x、y坐标了。

完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:

...

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/GetObjectPosition.srv"
)

...

功能包的package.xml文件中也需要添加代码生成的功能依赖:

 ...

 <build_depend>rosidl_default_generators</build_depend>
 <exec_depend>rosidl_default_runtime</exec_depend>
 <member_of_group>rosidl_interface_packages</member_of_group>

 ...
  • 程序调用

我们在代码中再来重点看下接口的使用方法。

客户端接口调用

learning_service/service_object_client.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-请求目标识别,等待目标位置应答
"""

import rclpy                                            # ROS2 Python接口库
from rclpy.node   import Node                           # ROS2 节点类
from learning_interface.srv import GetObjectPosition    # 自定义的服务接口

class objectClient(Node):
    def __init__(self, name):
        super().__init__(name)                          # ROS2节点父类初始化
        self.client = self.create_client(GetObjectPosition, 'get_target_position')
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.request = GetObjectPosition.Request()

    def send_request(self):
        self.request.get = True
        self.future = self.client.call_async(self.request)

def main(args=None):
    rclpy.init(args=args)                             # ROS2 Python接口初始化
    node = objectClient("service_object_client")      # 创建ROS2节点对象并进行初始化
    node.send_request()

    while rclpy.ok():
        rclpy.spin_once(node)

        if node.future.done():
            try:
                response = node.future.result()
            except Exception as e:
                node.get_logger().info(
                    'Service call failed %r' % (e,))
            else:
                node.get_logger().info(
                    'Result of object position:\n x: %d y: %d' %
                    (response.x, response.y))
            break
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

服务端接口调用

learning_service/service_object_server.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供目标识别服务
"""

import rclpy                                           # ROS2 Python接口库
from rclpy.node import Node                            # ROS2 节点类
from sensor_msgs.msg import Image                      # 图像消息类型
import numpy as np                                     # Python数值计算库
from cv_bridge import CvBridge                         # ROS与OpenCV图像转换类
import cv2                                             # Opencv图像处理库
from learning_interface.srv import GetObjectPosition   # 自定义的服务接口

lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

class ImageSubscriber(Node):
    def __init__(self, name):
        super().__init__(name)                              # ROS2节点父类初始化
        self.sub = self.create_subscription(
            Image, 'image_raw', self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        self.cv_bridge = CvBridge()                         # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

        self.srv = self.create_service(GetObjectPosition,   # 创建服务器对象(接口类型、服务名、服务器回调函数)
                                       'get_target_position',
                                       self.object_position_callback)    
        self.objectX = 0
        self.objectY = 0                              

    def object_detect(self, image):
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)      # 图像从BGR颜色模型转换为HSV模型
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) # 图像二值化
        contours, hierarchy = cv2.findContours(
            mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)   # 图像中轮廓检测

        for cnt in contours:                                  # 去除一些轮廓面积太小的噪声
            if cnt.shape[0] < 150:
                continue

            (x, y, w, h) = cv2.boundingRect(cnt)              # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)# 将苹果的轮廓勾勒出来
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5,
                       (0, 255, 0), -1)                       # 将苹果的图像中心点画出来

            self.objectX = int(x+w/2)
            self.objectY = int(y+h/2)

        cv2.imshow("object", image)                            # 使用OpenCV显示处理后的图像效果
        cv2.waitKey(50)

    def listener_callback(self, data):
        self.get_logger().info('Receiving video frame')        # 输出日志信息,提示已进入回调函数
        image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8')     # 将ROS的图像消息转化成OpenCV图像
        self.object_detect(image)                              # 苹果检测

    def object_position_callback(self, request, response):     # 创建回调函数,执行收到请求后对数据的处理
        if request.get == True:
            response.x = self.objectX                          # 目标物体的XY坐标
            response.y = self.objectY
            self.get_logger().info('Object position\nx: %d y: %d' %
                                   (response.x, response.y))   # 输出日志信息,提示已经反馈
        else:
            response.x = 0
            response.y = 0
            self.get_logger().info('Invalid command')          # 输出日志信息,提示已经反馈
        return response


def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = ImageSubscriber("service_object_server")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

5. 案例二:话题接口的定义与使用

话题通信接口的定义也是类似的,继续从之前的机器视觉案例中来衍生,我们想把服务换成话题,周期发布目标识别的位置,不管有没有人需要。

运行效果

现在我们会运行三个节点:

  • 第一个节点,将驱动相机并发布图像话题,此时的话题数据使用的是ROS中标准定义的Image图像消息;
  • 第二个节点,会运行视觉识别功能,识别目标的位置,这个位置我们希望封装成话题消息,发布出去,谁需要使用谁就来订阅;
  • 第三个节点,订阅位置话题,打印到终端中。

启动三个终端,分别运行以上节点:

$ ros2 run usb_cam usb_cam_node_exe
$ ros2 run learning_topic interface_object_pub
$ ros2 run learning_topic interface_object_sub

接口定义

在这个例程中,我们使用ObjectPosition.msg定义了服务通信的接口:

learning_interface/msg/ObjectPosition.msg

int32 x      # 表示目标的X坐标
int32 y      # 表示目标的Y坐标

话题消息的内容是一个位置,我们使用x、y坐标值进行描述。

完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:

...

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/ObjectPosition.msg"
)

...

程序调用

我们在代码中再来重点看下接口的使用方法。

发布者接口调用

learning_topic/interface_object_pub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2接口示例-发布目标位置
"""

import rclpy                                       # ROS2 Python接口库
from rclpy.node import Node                        # ROS2 节点类
from sensor_msgs.msg import Image                  # 图像消息类型
from cv_bridge import CvBridge                     # ROS与OpenCV图像转换类
import cv2                                         # Opencv图像处理库
import numpy as np                                 # Python数值计算库
from learning_interface.msg import ObjectPosition  # 自定义的目标位置消息

lower_red = np.array([0, 90, 128])                 # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])              # 红色的HSV阈值上限

"""
创建一个订阅者节点
"""
class ImageSubscriber(Node):

    def __init__(self, name):
        super().__init__(name)                                  # ROS2节点父类初始化
        self.sub = self.create_subscription(
            Image, 'image_raw', self.listener_callback, 10)     # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
        self.pub = self.create_publisher(
            ObjectPosition, "object_position", 10)              # 创建发布者对象(消息类型、话题名、队列长度)
        self.cv_bridge = CvBridge()                             # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

        self.objectX = 0
        self.objectY = 0   

    def object_detect(self, image):      
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)        # 图像从BGR颜色模型转换为HSV模型
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red)   # 图像二值化
        contours, hierarchy = cv2.findContours(
            mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)     # 图像中轮廓检测
        for cnt in contours:                                    # 去除一些轮廓面积太小的噪声
            if cnt.shape[0] < 150:
                continue

            (x, y, w, h) = cv2.boundingRect(cnt)                # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高

            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)  # 将苹果的轮廓勾勒出来
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5,      # 将苹果的图像中心点画出来
                       (0, 255, 0), -1)   

            self.objectX = int(x+w/2)
            self.objectY = int(y+h/2)

        cv2.imshow("object", image)                             # 使用OpenCV显示处理后的图像效果
        cv2.waitKey(50)

    def listener_callback(self, data):
        self.get_logger().info('Receiving video frame')         # 输出日志信息,提示已进入回调函数
        image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8')      # 将ROS的图像消息转化成OpenCV图像
        position = ObjectPosition()
        self.object_detect(image)                               # 苹果检测
        position.x, position.y = int(self.objectX), int(self.objectY)
        self.pub.publish(position)                              # 发布目标位置

def main(args=None):                                        # ROS2节点主入口main函数
    rclpy.init(args=args)                                   # ROS2 Python接口初始化
    node = ImageSubscriber("topic_webcam_sub")              # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                        # 循环等待ROS2退出
    node.destroy_node()                                     # 销毁节点对象
    rclpy.shutdown()                                        # 关闭ROS2 Python接口
订阅者接口调用

learning_topic/interface_object_sub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2接口示例-订阅目标位置
"""

import rclpy                                       # ROS2 Python接口库
from rclpy.node   import Node                      # ROS2 节点类
from std_msgs.msg import String                    # 字符串消息类型
from learning_interface.msg import ObjectPosition  # 自定义的目标位置消息

"""
创建一个订阅者节点
"""
class SubscriberNode(Node):
    def __init__(self, name):
        super().__init__(name)                                                # ROS2节点父类初始化
        self.sub = self.create_subscription(\
            ObjectPosition, "/object_position", self.listener_callback, 10)   # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度

    def listener_callback(self, msg):                                         # 创建回调函数,执行收到话题消息后对数据的处理
        self.get_logger().info('Target Position: "(%d, %d)"' % (msg.x, msg.y))# 输出日志信息,提示订阅收到的话题消息


def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = SubscriberNode("interface_position_sub")  # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

6. 接口命令行操作

$ ros2 interface list                    # 查看系统接口列表
$ ros2 interface show <interface_name>   # 查看某个接口的详细定义
$ ros2 interface package <package_name>  # 查看某个功能包中的接口定义

第十一讲:动作

1. 通信模型

举个例子,比如我们想让机器人转个圈,这肯定不是一下就可以完成的,机器人得一点一点旋转,直到360度才能结束,假设机器人并不在我们眼前,发出指令后,我们根本不知道机器人到底有没有开始转圈,转到哪里了?

现在我们需要的是一个反馈,比如每隔1s,告诉我们当前转到多少度了,10度、20度、30度,一段时间之后,到了360度,再发送一个信息,表示动作执行完成。

这样一个需要执行一段时间的行为,使用动作的通信机制就更为合适,就像装了一个进度条,我们可以随时把控进度,如果运动过程当中,我们还可以随时发送一个取消运动的命令。

客户端/服务器模型

客户端发送一个运动的目标,想让机器人动起来,服务器端收到之后,就开始控制机器人运动,一边运动,一边反馈当前的状态,如果是一个导航动作,这个反馈可能是当前所处的坐标,如果是机械臂抓取,这个反馈可能又是机械臂的实时姿态。当运动执行结束后,服务器再反馈一个动作结束的信息。整个通信过程就此结束。

一对多通信

和服务一样,动作通信中的客户端可以有多个,大家都可以发送运动命令,但是服务器端只能有一个,毕竟只有一个机器人,先执行完成一个动作,才能执行下一个动作。

同步通信

既然有反馈,那动作也是一种同步通信机制,之前我们也介绍过,动作过程中的数据通信接口,使用.action文件进行定义。

由服务和话题合成

大家再仔细看下上边的动图,是不是还会发现一个隐藏的秘密。

动作的三个通信模块,竟然有两个是服务,一个是话题,当客户端发送运动目标时,使用的是服务的请求调用,服务器端也会反馈一个应带,表示收到命令。动作的反馈过程,其实就是一个话题的周期发布,服务器端是发布者,客户端是订阅者。

没错,动作是一种应用层的通信机制,其底层就是基于话题和服务来实现的。

2. 案例一:小海龟的动作

$ ros2 run turtlesim turtlesim_node
$ ros2 run turtlesim turtle_teleop_key
$ ros2 action info /turtle1/rotate_absolute
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}"
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback

3. 案例二:机器人画圆

动作虽然是基于话题和服务实现的,但在实际使用中,并不会直接使用话题和服务的编程方法,而是有一套针对动作特性封装好的编程接口,接下来我们就一起试一 试。

运行效果

启动两个终端,分别运行一下命令,启动动作示例的服务端和客户端:

$ ros2 run learning_action action_move_server 
$ ros2 run learning_action action_move_client

 终端中,我们可以看到客户端发送动作目标之后,服务器端开始模拟机器人运动,每30度发送一次反馈信息,最终完成运动,并反馈结束运动的信息。

接口定义

例程使用的动作并不是ROS中的标准定义,我们通过MoveCircle.action进行自定义:

learning_interface/action/MoveCircle.action

bool enable     # 定义动作的目标,表示动作开始的指令
---
bool finish     # 定义动作的结果,表示是否成功执行
---
int32 state     # 定义动作的反馈,表示当前执行到的位置

包含三个部分:

  • 第一块是动作的目标,enable为true时,表示开始运动;
  • 第二块是动作的执行结果,finish为true,表示动作执行完成;
  • 第三块是动作的周期反馈,表示当前机器人旋转到的角度。

完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:

...

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/MoveCircle.action"
)

...

通信模型

通信模型就是这样,客户端发送给一个动作目标,服务器控制机器人开始运动,并周期反馈,结束后反馈结束信息。

服务端代码解析

learning_action/action_move_server.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2动作示例-负责执行圆周运动动作的服务端
"""

import time

import rclpy                                      # ROS2 Python接口库
from rclpy.node   import Node                     # ROS2 节点类
from rclpy.action import ActionServer             # ROS2 动作服务器类
from learning_interface.action import MoveCircle  # 自定义的圆周运动接口

class MoveCircleActionServer(Node):
    def __init__(self, name):
        super().__init__(name)                   # ROS2节点父类初始化
        self._action_server = ActionServer(      # 创建动作服务器(接口类型、动作名、回调函数)
            self,
            MoveCircle,
            'move_circle',
            self.execute_callback)

    def execute_callback(self, goal_handle):            # 执行收到动作目标之后的处理函数
        self.get_logger().info('Moving circle...')
        feedback_msg = MoveCircle.Feedback()            # 创建一个动作反馈信息的消息

        for i in range(0, 360, 30):                     # 从0到360度,执行圆周运动,并周期反馈信息
            feedback_msg.state = i                      # 创建反馈信息,表示当前执行到的角度
            self.get_logger().info('Publishing feedback: %d' % feedback_msg.state)
            goal_handle.publish_feedback(feedback_msg)  # 发布反馈信息
            time.sleep(0.5)

        goal_handle.succeed()                           # 动作执行成功
        result = MoveCircle.Result()                    # 创建结果消息
        result.finish = True                            
        return result                                   # 反馈最终动作执行的结果

def main(args=None):                                    # ROS2节点主入口main函数
    rclpy.init(args=args)                               # ROS2 Python接口初始化
    node = MoveCircleActionServer("action_move_server") # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                    # 循环等待ROS2退出
    node.destroy_node()                                 # 销毁节点对象
    rclpy.shutdown()                                    # 关闭ROS2 Python接口

客户端代码解析

learning_action/action_move_client.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2动作示例-请求执行圆周运动动作的客户端
"""

import rclpy                                      # ROS2 Python接口库
from rclpy.node   import Node                     # ROS2 节点类
from rclpy.action import ActionClient             # ROS2 动作客户端类

from learning_interface.action import MoveCircle  # 自定义的圆周运动接口

class MoveCircleActionClient(Node):
    def __init__(self, name):
        super().__init__(name)                   # ROS2节点父类初始化
        self._action_client = ActionClient(      # 创建动作客户端(接口类型、动作名)
            self, MoveCircle, 'move_circle') 

    def send_goal(self, enable):                 # 创建一个发送动作目标的函数
        goal_msg = MoveCircle.Goal()             # 创建一个动作目标的消息
        goal_msg.enable = enable                 # 设置动作目标为使能,希望机器人开始运动

        self._action_client.wait_for_server()    # 等待动作的服务器端启动
        self._send_goal_future = self._action_client.send_goal_async(   # 异步方式发送动作的目标
            goal_msg,                                                   # 动作目标
            feedback_callback=self.feedback_callback)                   # 处理周期反馈消息的回调函数

        self._send_goal_future.add_done_callback(self.goal_response_callback) # 设置一个服务器收到目标之后反馈时的回调函数

    def goal_response_callback(self, future):           # 创建一个服务器收到目标之后反馈时的回调函数
        goal_handle = future.result()                   # 接收动作的结果
        if not goal_handle.accepted:                    # 如果动作被拒绝执行
            self.get_logger().info('Goal rejected :(')
            return

        self.get_logger().info('Goal accepted :)')                            # 动作被顺利执行

        self._get_result_future = goal_handle.get_result_async()              # 异步获取动作最终执行的结果反馈
        self._get_result_future.add_done_callback(self.get_result_callback)   # 设置一个收到最终结果的回调函数 

    def get_result_callback(self, future):                                    # 创建一个收到最终结果的回调函数
        result = future.result().result                                       # 读取动作执行的结果
        self.get_logger().info('Result: {%d}' % result.finish)                # 日志输出执行结果

    def feedback_callback(self, feedback_msg):                                # 创建处理周期反馈消息的回调函数
        feedback = feedback_msg.feedback                                      # 读取反馈的数据
        self.get_logger().info('Received feedback: {%d}' % feedback.state) 

def main(args=None):                                       # ROS2节点主入口main函数
    rclpy.init(args=args)                                  # ROS2 Python接口初始化
    node = MoveCircleActionClient("action_move_client")    # 创建ROS2节点对象并进行初始化
    node.send_goal(True)                                   # 发送动作目标
    rclpy.spin(node)                                       # 循环等待ROS2退出
    node.destroy_node()                                    # 销毁节点对象
    rclpy.shutdown()                                       # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

    entry_points={
        'console_scripts': [
         'action_move_client    = learning_action.action_move_client:main',
         'action_move_server    = learning_action.action_move_server:main',
        ],
    },

第十二讲:参数

1. 通信模型

在NodeA相机驱动节点中,就需要考虑很多问题,相机连接到哪个usb端口,使用的图像分辨率是多少,曝光度和编码格式分别是什么,这些都可以通过参数设置,我们可以通过配置文件或者程序进行设置。

NodeB节点中也是一样,图像识别使用的阈值是多少,整个图像面积很大,那个部分是我们关注的核心区域,识别过程是否需要美颜等等,就像我们使用美颜相机一样,我们可以通过滑动条或者输入框设置很多参数,不同参数设置后,都会改变执行功能的一些效果。

这就是参数的作用。

全局字典

在ROS系统中,参数是以全局字典的形态存在的,什么叫字典?就像真实的字典一样,由名称和数值组成,也叫做键和值,合成键值。或者我们也可以理解为,就像编程中的参数一样,有一个参数名 ,然后跟一个等号,后边就是参数值了,在使用的时候,访问这个参数名即可。

可动态监控

在ROS2中,参数的特性非常丰富,比如某一个节点共享了一个参数,其他节点都可以访问,如果某一个节点对参数进行了修改,其他节点也有办法立刻知道,从而获取最新的数值。这在参数的高级编程中,大家都可能会用到。

2. 案例一:小海龟例程中的参数

在小海龟的例程中,仿真器也提供了不少参数,我们一起来通过这个例程,熟悉下参数的含义和命令行的使用方法。

启动两个终端,分别运行小海龟仿真器和键盘控制节点:

$ ros2 run turtlesim turtlesim_node
$ ros2 run turtlesim turtle_teleop_key

查看参数列表

当前系统中有哪些参数呢?我们可以启动一个终端,并使用如下命令查询:

$ ros2 param list

参数查询与修改

如果想要查询或者修改某个参数的值,可以在param命令后边跟get或者set子命令:

$ ros2 param describe turtlesim background_b   # 查看某个参数的描述信息
$ ros2 param get turtlesim background_b        # 查询某个参数的值
$ ros2 param set turtlesim background_b 10     # 修改某个参数的值

参数文件保存与加载

一个一个查询/修改参数太麻烦了,不如试一试参数文件,ROS中的参数文件使用yaml格式,可以在param命令后边跟dump子命令,将某个节点的参数都保存到文件中,或者通过load命令一次性加载某个参数文件中的所有内容:

$ ros2 param dump turtlesim >> turtlesim.yaml  # 将某个节点的参数保存到参数文件中
$ ros2 param load turtlesim turtlesim.yaml     # 一次性加载某一个文件中的所有参数

接下来就要开始写程序了,在程序中设置参数和读取参数都比较简单,一两句函数就可以实现,我们先来体验一下这几个函数的使用方法。

运行效果

启动一个终端,先运行第一句指令,启动param_declare节点,终端中可以看到循环打印的日志信息,其中的“mbot”就是我们设置的一个参数值,参数名称是“robot_name”,通过命令行修改这个参数,看下终端中会发生什么?

$ ros2 run learning_parameter param_declare
$ ros2 param set param_declare robot_name turtle

代码解析

我们来看下在代码中,如何声明、创建、修改一个参数的值。

learning_parameter/param_declare.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2参数示例-创建、读取、修改参数
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类

class ParameterNode(Node):
    def __init__(self, name):
        super().__init__(name)                                    # ROS2节点父类初始化
        self.timer = self.create_timer(2, self.timer_callback)    # 创建一个定时器(单位为秒的周期,定时执行的回调函数)
        self.declare_parameter('robot_name', 'mbot')              # 创建一个参数,并设置参数的默认值

    def timer_callback(self):                                      # 创建定时器周期执行的回调函数
        robot_name_param = self.get_parameter('robot_name').get_parameter_value().string_value   # 从ROS2系统中读取参数的值

        self.get_logger().info('Hello %s!' % robot_name_param)     # 输出日志信息,打印读取到的参数值

        new_name_param = rclpy.parameter.Parameter('robot_name',   # 重新将参数值设置为指定值
                            rclpy.Parameter.Type.STRING, 'mbot')
        all_new_parameters = [new_name_param]
        self.set_parameters(all_new_parameters)                    # 将重新创建的参数列表发送给ROS2系统

def main(args=None):                                 # ROS2节点主入口main函数
    rclpy.init(args=args)                            # ROS2 Python接口初始化
    node = ParameterNode("param_declare")            # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                 # 循环等待ROS2退出
    node.destroy_node()                              # 销毁节点对象
    rclpy.shutdown()                                 # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
    'console_scripts': [
     'param_declare          = learning_parameter.param_declare:main',
    ],
},

3. 案例三:机器视觉应用

参数大家已经会使用了,如何在机器人中应用呢?

继续优化机器视觉的示例,物体识别对光线比较敏感,不同的环境大家使用的阈值也是不同的,每次在代码中修改阈值还挺麻烦,不如我们就把阈值提炼成参数,运行过程中就可以动态设置,不是大大提高了程序的易用性么?

说干就干,我们先来看下效果如何,再看下代码中的变化。

运行效果

启动三个终端,分别运行:

  1. 相机驱动节点
  2. 视觉识别节点
  3. 修改红色阈值
$ ros2 run usb_cam usb_cam_node_exe 
$ ros2 run learning_parameter param_object_detect
$ ros2 param set param_object_detect red_h_upper 180

在启动的视觉识别节点中,我们故意将视觉识别中红色阈值的上限设置为0,如果不修改参数,将无法实现目标识别。

为了便于调整阈值,我们在节点中将红色阈值的限位修改为了ROS参数,通过命令行修改该参数的值,就可以实现视觉识别啦。

代码解析

我们来看下在视觉识别的代码中,是如何通过参数来设置阈值的。

learning_parameter/param_object_detect.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2参数示例-设置目标识别的颜色阈值参数
"""

import rclpy                      # ROS2 Python接口库
from rclpy.node import Node       # ROS2 节点类
from sensor_msgs.msg import Image # 图像消息类型
from cv_bridge import CvBridge    # ROS与OpenCV图像转换类
import cv2                        # Opencv图像处理库
import numpy as np                # Python数值计算库

lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

"""
创建一个订阅者节点
"""
class ImageSubscriber(Node):
  def __init__(self, name):
    super().__init__(name)                                  # ROS2节点父类初始化    
    self.sub = self.create_subscription(Image,              # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)     
                  'image_raw', self.listener_callback, 10) 
    self.cv_bridge = CvBridge()                             # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

    self.declare_parameter('red_h_upper', 0)                # 创建一个参数,表示阈值上限
    self.declare_parameter('red_h_lower', 0)                # 创建一个参数,表示阈值下限

  def object_detect(self, image):
    upper_red[0] = self.get_parameter('red_h_upper').get_parameter_value().integer_value    # 读取阈值上限的参数值
    lower_red[0] = self.get_parameter('red_h_lower').get_parameter_value().integer_value    # 读取阈值下限的参数值
    self.get_logger().info('Get Red H Upper: %d, Lower: %d' % (upper_red[0], lower_red[0])) # 通过日志打印读取到的参数值

    hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)                                        # 图像从BGR颜色模型转换为HSV模型
    mask_red = cv2.inRange(hsv_img, lower_red, upper_red)                                   # 图像二值化
    contours, hierarchy = cv2.findContours(mask_red, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)  # 图像中轮廓检测
    for cnt in contours:                                                                    # 去除一些轮廓面积太小的噪声
        if cnt.shape[0] < 150:
            continue

        (x, y, w, h) = cv2.boundingRect(cnt)                            # 得到苹果所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
        cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)              # 将苹果的轮廓勾勒出来
        cv2.circle(image, (int(x+w/2), int(y+h/2)), 5, (0, 255, 0), -1) # 将苹果的图像中心点画出来

    cv2.imshow("object", image)                                         # 使用OpenCV显示处理后的图像效果
    cv2.waitKey(50)

  def listener_callback(self, data):
    self.get_logger().info('Receiving video frame')     # 输出日志信息,提示已进入回调函数
    image = self.cv_bridge.imgmsg_to_cv2(data, "bgr8")  # 将ROS的图像消息转化成OpenCV图像
    self.object_detect(image)                            # 苹果检测

def main(args=None):                                    # ROS2节点主入口main函数
    rclpy.init(args=args)                               # ROS2 Python接口初始化
    node = ImageSubscriber("param_object_detect")       # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                    # 循环等待ROS2退出
    node.destroy_node()                                 # 销毁节点对象
    rclpy.shutdown()                                    # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
    'console_scripts': [
     'param_declare          = learning_parameter.param_declare:main',
     'param_object_detect    = learning_parameter.param_object_detect:main',
    ],
},

第十三讲:分布式通信

智能机器人的功能繁多,全都放在一个计算机里,经常会遇到计算能力不够、处理出现卡顿等情况,如果可以将这些任务拆解,分配到多个计算机中运行岂不是可以减轻压力?

这就是分布式系统,可以实现多计算平台上的任务分配。

1. 分布式通信

什么叫分布式?

之前我们也讲过,在ROS系统中,机器人功能是由各种节点组成的,这些节点可能位于不同的计算机中,这种结构可以将原本资源消耗较多的任务,分配到不同的平台上,减轻计算压力,这就是分布式通信框架的典型应用之一。

比如这款机器人系统中,有两个计算平台。

机器人体积比较小,不适合放一个笔记本电脑在上边,于是采用树莓派作为控制器,主要实现传感器驱动和电机控制等功能,不过视觉处理和应用功能就不适合在树莓派里运行了,我们放在另外一个性能更强的笔记本电脑中,此外我们还需要在电脑上监控机器人的传感器信息,并且远程控制机器人运动。

两个电脑之间的通信,看上去还有点复杂,毕竟相互传输的数据还挺多的,不过ROS系统都已经为我们设计好了,我们只需要在每一个电脑上配置好ROS环境,功能开发上完全不需要做任何变化,实现非常方便。

接下来,我们就带领大家一起来感受下ROS分布式系统的魅力。

2. 分布式网络搭建

除了我使用的笔记本电脑之外,另外一个计算平台我们选择了树莓派,模拟一个放置在机器人上的控制器。

树莓派配置

在开发之前,我们需要先配置好树莓派的环境,网上也有很多资料,大家都可以参考。

装系统

我们先要给树莓派装系统,这里我们选择的是Ubuntu Mate针对树莓派的镜像,下载镜像之后,烧写到树莓派的SD卡中就可以启动系统了。

 Ubuntu MATE镜像下载链接:Choose an architecture | Download

安装ROS2

在安装好的Ubuntu Mate系统中,安装ROS2,和电脑端安装的流程一样。

 

编译代码

将我们课程的代码也下载到树莓派中,进行编译。

 

远程桌面

如果大家有显示器,可以直接在树莓派上连接键盘鼠标显示器使用,如果使用不方便的话,也可以在树莓派上配置好远程桌面,就可以通过网络访问树莓派的桌面系统了。

 

以上步骤的整体流程和电脑端操作基本一致,大家也可以参考如下链接配置树莓派:

【树莓派4B】安装Ubuntu Mate20.04+ROS Noetic+使用电脑自带的xrdp和VNC进行PC端远程控制_ubutnu mate vnc-CSDN博客

3. 分布式数据传输

树莓派配置完成后,确保已经和你所使用的电脑连接到了同一个局域网络中。接下来我们打通两个计算平台的通信能力。具体需要做什么呢?

简而言之,什么都不需要做。我们直接用命令行测试一下话题通信的效果。

Attention

如使用虚拟机,请将虚拟机网络修改为桥接模式

 在树莓派端,使用如下命令启动一个发布者节点:

$ ros2 run demo_nodes_cpp talker  #树莓派端

接下来在电脑端,使用如下命令启动一个订阅者节点: 

$ ros2 run demo_nodes_py listener #PC端

神奇的事情就这样发生了,只要两个计算机安装好ROS2,并且处于同一网络中,他们就可以实现之前讲过的话题、服务、动作等通信了,感觉就像在一个电脑里一样。

不过这也会带来一个问题,如果一个网络中有很多个计算机,我们并不希望他们都可以互通互联,而是可以分组通信,小组之间是无法实现通信的。

4. 分布式网络分组

没问题,ROS2提供了一个DOMAIN的机制,就类似分组一样,处于同一个DOMAIN中的计算机才能通信,我们可以在电脑和树莓派端的.bashrc中加入这样一句配置,即可将两者分配到一个小组中:

$ export ROS_DOMAIN_ID=<your_domain_id>

如果分配的ID不同,则两者无法实现通信。 

第十四讲:DDS

ROS2中最为重大的变化——DDS,前边课程中学习的话题、服务、动作,他们底层通信的具体实现过程,都是靠DDS来完成的,它相当于是ROS机器人系统中的神经网络

1. 通信模型

DDS的核心是通信,能够实现通信的模型和软件框架非常多,这里我们列出常用的四种模型。

  • 第一种,点对点模型,许多客户端连接到一个服务端,每次通信时,通信双方必须建立一条连接。当通信节点增多时,连接数也会增多。而且每个客户端都需要知道服务器的具体地址和所提供的服务,一旦服务器地址发生变化,所有客户端都会受到影响。

  • 第二种,Broker模型,针对点对点模型进行了优化,由Broker集中处理所有人的请求,并进一步找到真正能响应该服务的角色。这样客户端就不用关心服务器的具体地址了。不过问题也很明显,Broker作为核心,它的处理速度会影响所有节点的效率,当系统规模增长到一定程度,Broker就会成为整个系统的性能瓶颈。更麻烦是,如果Broker发生异常,可能导致整个系统都无法正常运转。之前的ROS1系统,使用的就是类似这样的架构。

  • 第三种,广播模型,所有节点都可以在通道上广播消息,并且节点都可以收到消息。这个模型解决了服务器地址的问题,而且通信双方也不用单独建立连接,但是广播通道上的消息太多了,所有节点都必须关心每条消息,其实很多是和自己没有关系的。

  • 第四种,就是以数据为中心的DDS模型了,这种模型与广播模型有些类似,所有节点都可以在DataBus上发布和订阅消息。但它的先进之处在于,通信中包含了很多并行的通路,每个节点可以只关心自己感兴趣的消息,忽略不感兴趣的消息,有点像是一个旋转火锅,各种好吃的都在这个DataBus传送,我们只需要拿自己想吃的就行,其他的和我们没有关系。

可见,在这几种通信模型中,DDS的优势更加明显。

 

DDS并不是一个新的通信方式,在ROS2之前,DDS已经广泛应用在很多领域,比如航空,国防,交通,医疗,能源等。

比如在自动驾驶领域,通常会存在感知,预测,决策和定位等模块,这些模块都需要非常高速和频繁地交换数据。借助DDS,可以很好地满足它们的通信需求。

什么是DDS

好啦,说了半天DDS,到底啥意思呢?我们来做一个完整的介绍

DDS的全称是Data Distribution Service,也就是数据分发服务,2004年由对象管理组织OMG发布和维护,是一套专门为实时系统设计的数据分发/订阅标准,最早应用于美国海军, 解决舰船复杂网络环境中大量软件升级的兼容性问题,现在已经成为强制标准。

DDS强调以数据为中心,可以提供丰富的服务质量策略,以保障数据进行实时、高效、灵活地分发,可满足各种分布式实时通信应用需求。

这里也提一下对象管理组织OMG,成立于1989年,它的使命是开发技术标准,为数以千计的垂直行业提供真实的价值,比如大家课可能听说过的统一建模语言SYSML和UML,还有中间件标准CORBA等,当然还有DDS。

DDS在ROS2中的应用

DDS在ROS2系统中的位置至关重要,所有上层建设都建立在DDS之上。在这个ROS2的架构图中,蓝色和红色部分就是DDS。

 

刚才我们也提到,DDS是一种通信的标准,就像4G、5G一样,既然是标准,那大家都可以按照这个标准来实现对应的功能,所以华为、高通都有很多5G的技术专利,DDS也是一样,能够按照DDS标准实现的通信系统很多,这里每一个红色模块,就是某一企业或组织实现的一种DDS系统。

既然可选用的DDS这么多,那我们该用哪一个呢?具体而言,他们肯定都符合基本标准,但还是会有性能上的差别,ROS2的原则就是尽量兼容,让用户根据使用场景选择,比如个人开发,我们选择一个开源版本的DDS就行,如果是工业应用,那可能得选择一个商业授权的版本了。

为了实现对多个DDS的兼容,ROS设计了一个Middleware中间件,也就是一个统一的标准,不管我们用那个DDS,保证上层编程使用的函数接口都是一样的。此时兼容性的问题就转移给了DDS厂商,如果他们想让自己的DDS系统进入ROS生态,就得按照ROS的接口标准,开发一个驱动,也就是这个部分。

无论如何,ROS的宗旨不变,要提高软件代码的复用性,下边DDS任你边,上边的软件没影响。

在ROS的四大组成部分中,由于DDS的加入,大大提高了分布式通信系统的综合能力,这样我们在开发机器人的过程中,就不需要纠结通信的问题,可以把更多时间放在其他部分的应用开发上。

质量服务策略QoS

DDS为ROS的通信系统提供提供了哪些特性呢?我们通过这个通信模型图来看下。

 

DDS中的基本结构是Domain,Domain将各个应用程序绑定在一起进行通信,回忆下之前我们配置树莓派和电脑通信的时候,配置的那个DOMAIN ID,就是对全局数据空间的分组定义,只有处于同一个DOMAIN小组中的节点才能互相通信。这样可以避免无用数据占用的资源。

DDS中另外一个重要特性就是质量服务策略,QoS

QoS是一种网络传输策略,应用程序指定所需要的网络传输质量行为,QoS服务实现这种行为要求,尽可能地满足客户对通信质量的需求,可以理解为数据提供者和接收者之间的合约

具体会有哪些策略?比如:

  • DEADLINE策略,表示通信数据必须要在每次截止时间内完成一次通信;
  • HISTORY策略,表示针对历史数据的一个缓存大小;
  • RELIABILITY策略,表示数据通信的模式,配置成BEST_EFFORT,就是尽力传输模式,网络情况不好的时候,也要保证数据流畅,此时可能会导致数据丢失,配置成RELIABLE,就是可信赖模式,可以在通信中尽量保证图像的完整性,我们可以根据应用功能场景选择合适的通信模式;
  • DURABILITY策略,可以配置针对晚加入的节点,也保证有一定的历史数据发送过去,可以让新节点快速适应系统。

所有这些策略在ROS系统中都可以通过类似这样的结构体配置,如果不配置的话,系统也会使用默认的参数。

举一个机器人的例子便于大家理解。

 比如我们遥控一个无人机航拍,如果网络情况不好的话,遥控器向无人机发送运动指令的过程,可以用reliable通信模式,保证每一个命令都可以顺利发送给无人机,但是可能会有一些延时,无人机传输图像的过程可以用best effort模式,保证视频的流畅性,但是可能会有掉帧。

如果此时出现一个黑客黑入我们的网络,也没有关系,我们可以给ROS2的通信数据进行加密,黑客也没有办法直接控制无人机。

2. 案例一:在命令行中配置DDS

我们先来试一试在命令行中配置DDS的参数。

启动第一个终端,我们使用best_effort创建一个发布者节点,循环发布任意数据,在另外一个终端中,如果我们使用reliable模型订阅同一话题,无法实现数据通信,如果修改为同样的best_effort,才能实现数据传输。

$ ros2 topic pub /chatter std_msgs/msg/Int32 "data: 42" --qos-reliability best_effort 
$ ros2 topic echo /chatter --qos-reliability reliable
$ ros2 topic echo /chatter --qos-reliability best_effort

如何去查看ROS2系统中每一个发布者或者订阅者的QoS策略呢,在topic命令后边跟一个"--verbose"参数就行了。

$ ros2 topic info /chatter --verbose

 

3. 案例二:DDS编程示例

接下来,我们尝试在代码中配置DDS,以之前Hello World话题通信为例。

运行效果

启动两个终端,分别运行发布者和订阅者节点:

$ ros2 run learning_qos qos_helloworld_pub
$ ros2 run learning_qos qos_helloworld_sub

可以看到两个终端中的通信效果如下,和之前貌似并没有太大区别。

 

看效果确实差不多,不过底层通信机理上可是有所不同的。

发布者代码解析

我们看下在代码中,如果加入QoS的配置。

learning_qos/qos_helloworld_pub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 QoS示例-发布“Hello World”话题
"""

import rclpy                     # ROS2 Python接口库
from rclpy.node import Node      # ROS2 节点类
from std_msgs.msg import String  # 字符串消息类型
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy # ROS2 QoS类

"""
创建一个发布者节点
"""
class PublisherNode(Node):

    def __init__(self, name):
        super().__init__(name)        # ROS2节点父类初始化

        qos_profile = QoSProfile(     # 创建一个QoS原则
            # reliability=QoSReliabilityPolicy.BEST_EFFORT,
            reliability=QoSReliabilityPolicy.RELIABLE,
            history=QoSHistoryPolicy.KEEP_LAST,
            depth=1
        )
        self.pub = self.create_publisher(String, "chatter", qos_profile) # 创建发布者对象(消息类型、话题名、QoS原则)
        self.timer = self.create_timer(0.5, self.timer_callback)         # 创建一个定时器(单位为秒的周期,定时执行的回调函数)

    def timer_callback(self):                                # 创建定时器周期执行的回调函数
        msg = String()                                       # 创建一个String类型的消息对象
        msg.data = 'Hello World'                             # 填充消息对象中的消息数据
        self.pub.publish(msg)                                # 发布话题消息
        self.get_logger().info('Publishing: "%s"' % msg.data)# 输出日志信息,提示已经完成话题发布

def main(args=None):                           # ROS2节点主入口main函数
    rclpy.init(args=args)                      # ROS2 Python接口初始化
    node = PublisherNode("qos_helloworld_pub") # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                           # 循环等待ROS2退出
    node.destroy_node()                        # 销毁节点对象
    rclpy.shutdown()                           # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置: 

entry_points={
    'console_scripts': [
     'qos_helloworld_pub  = learning_qos.qos_helloworld_pub:main',
},

订阅者代码解析

订阅者中的QoS配置和发布者类似。

learning_qos/qos_helloworld_sub.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2 QoS示例-订阅“Hello World”话题消息
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类
from std_msgs.msg import String                  # ROS2标准定义的String消息
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy  # ROS2 QoS类

"""
创建一个订阅者节点
"""
class SubscriberNode(Node):

    def __init__(self, name):
        super().__init__(name)         # ROS2节点父类初始化

        qos_profile = QoSProfile(      # 创建一个QoS原则
            # reliability=QoSReliabilityPolicy.BEST_EFFORT,
            reliability=QoSReliabilityPolicy.RELIABLE,
            history=QoSHistoryPolicy.KEEP_LAST,
            depth=1
        )

        self.sub = self.create_subscription(\
            String, "chatter", self.listener_callback, qos_profile) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、QoS原则)

    def listener_callback(self, msg):                      # 创建回调函数,执行收到话题消息后对数据的处理
        self.get_logger().info('I heard: "%s"' % msg.data) # 输出日志信息,提示订阅收到的话题消息

def main(args=None):                               # ROS2节点主入口main函数
    rclpy.init(args=args)                          # ROS2 Python接口初始化
    node = SubscriberNode("qos_helloworld_sub")    # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                               # 循环等待ROS2退出
    node.destroy_node()                            # 销毁节点对象
    rclpy.shutdown()                               # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={
    'console_scripts': [
     'qos_helloworld_pub  = learning_qos.qos_helloworld_pub:main',
     'qos_helloworld_sub  = learning_qos.qos_helloworld_sub:main',
    ],
},

到此ROS2的核心概念已经介绍完成

常用工具部分的介绍:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1596905.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

numpy学习笔记(5),其他实用函数

8. 更多函数 8.1 随机数 8.1.1 常用随机数 8.1.1.1 numpy.random.rand(d0, d1, …, dn) 返回[0.0, 1.0)随机浮点数&#xff0c;即大于等于0.0&#xff0c;小于1.0。d0, d1, …, dn&#xff1a;返回的数组形状 # 使用numpy.random.rand函数 import numpy as np np.random.r…

09 Php学习:超级全局变量

超级全局变量 PHP中预定义了几个超级全局变量&#xff08;superglobals&#xff09; &#xff0c;这意味着它们在一个脚本的全部作用域中都可用。 PHP 超级全局变量列表: $GLOBALS$_SERVER$_REQUEST$_POST$_GET$_FILES$_ENV$_COOKIE$_SESSION $GLOBALS $GLOBALS 是 PHP 中的…

javaee初阶———多线程(三)

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 此篇文章与大家分享多线程专题第三篇,关于线程安全方面的内容 如果有不足的或者错误的请您指出! 目录 八、线程安全问题(重点)1.一个典型的线程不安全的例子2.出现线程不安全的原因3.解决线程不安…

【Entity Framework】聊一聊EF中继承关系

【Entity Framework】聊一聊EF中继承关系 文章目录 【Entity Framework】聊一聊EF中继承关系一、概述二、实体类型层次结构映射三、每个层次结构一张表和鉴别器配置四、共享列五、每个类型一张表配置六、每个具体类型一张表配置七、TPC数据库架构八、总结 一、概述 Entity Fra…

高清4路HDMI编码器JR-3214HD

产品简介&#xff1a; JR-3214HD四路高清HDMI编码器是专业的高清音视频编码产品&#xff0c;该产品具有支持4路高清HDMI音视频采集功能&#xff0c;4路3.5MM独立外接音频输入&#xff0c;编码输出双码流H.264格式&#xff0c;音频MP3/AAC格式。编码码率可调&#xff0c;画面质…

盒子模型+响应式布局 + 原型链与继承

盒子模型 是什么 css布局基础,规定了元素在页面上如何呈现,以及元素之间的空间关系 由content paddingbordermargin四部分组成 为什么 盒子模型分为 标准盒子模型: 元素的宽度与高度 只包括content IE盒子模型: 元素的宽度与高度 包括content,padding,border 在实际操作中…

【数据结构】【C++】AVL树的模拟实现(插入、判断、旋转)

文章目录 1 概念2 实现2.1 AVL树结点的定义2.2 AVL树的插入2.2.1 AVL树的插入规则2.2.2 旋转2.2.2.1 左单旋2.2.2.2 右单旋2.2.2.3 左右双旋2.2.2.4 右左双旋 2.2.3 总结 3 平衡判断4 删除5 源码 1 概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二…

软件测试/测试开发丨接口测试学习笔记分享

一、Mock 测试 1、Mock 测试的场景 前后端数据交互第三方系统数据交互硬件设备解耦 2、Mock 测试的价值与意义 不依赖第三方数据节省工作量节省联调 3、Mock 核心要素 匹配规则&#xff1a;mock的接口&#xff0c;改哪些接口&#xff0c;接口哪里的数据模拟响应 4、mock实…

【每日刷题】Day15

【每日刷题】Day15 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; 目录 1. 141. 环形链表 - 力扣&#xff08;LeetCode&#xff09; 2. 142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 3. 143. 重…

基于Python的微博舆论分析,微博评论情感分析可视化系统

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

IDEA 本地库引入了依赖但编译时找不到

在使用 IDEA 开发 Maven 项目的过程中&#xff0c;有时会遇到本地库引入了依赖&#xff0c;但编译时报找不到这个依赖&#xff0c;可以使用命令处理。 打开 Terminal。 执行清理命令。 mvn clean install -Dmaven.test.skiptrue执行更新命令。 mvn -U idea:idea

YOLO-World——S

文章目录 Abstract成果 MethodPre-training Formulation: Region-Text PairsModel ArchitectureYOLO DetectorText EncoderText Contrastive HeadTraining with Online VocabularyInference with Offline Vocabulary Re-parameterizable Vision-Language PANText-guided CSPLay…

string类——常用函数模拟(C++)

本篇中&#xff0c;将会详细的介绍 Cpp 中 string 的使用&#xff0c;以及 string 类常用函数的模拟实现。对于 string 的内置函数来说&#xff0c;存在很多很冗余的用法&#xff0c;很多函数都有很多种用法&#xff0c;本篇将会讲解常用内置函数的常用用法&#xff0c;模拟函数…

Pytest小技巧:高效获取自动化测试结果

自动化测试用例在执行完成后&#xff0c;我们想要很清楚的查看到测试用例的执行结果&#xff0c;我们可以通过Pytest中的Hooks来进行获取吗&#xff1f; 其中Pytest中存在多个Hooks的函数&#xff0c;小编今天先简单介绍其中一种&#xff0c;通过pytest_runtest_makereport 获…

若依vue中关于字典的使用

文章目录 字典管理页面列表点击某个字典类型展示具体字典数据修改某一条字典数据 字典的应用一般用于select多选框中代码实现根据字典Dict的value获取Label&#xff0c;类似于通过key获得value 源码解析 字典管理页面 列表 点击某个字典类型展示具体字典数据 修改某一条字典数…

04_UART串口发送数据

1.配置芯片&#xff0c;如果PA9,PA10的UART引脚被占用&#xff0c;会自动进行重映射 2.代码 int main(void) {uint8_t temp[]"test";/* USER CODE BEGIN 1 *//* USER CODE END 1 *//* MCU Configuration--------------------------------------------------------*…

windows的jar包开机自启动【搬代码】

感觉最方便的就是放到启动项目里操作步骤 winR 输入&#xff1a;shell:startup回车或点击确定 3.将自己jar包右键创建快捷方式 4.然后放进去 5.重启电脑&#xff0c;浏览器输入网址&#xff0c;就可以看到重启成功了 另外一个就是放入.exe文件的快捷方式 首先&#xff0c;…

C语言洛谷题目分享(9)奇怪的电梯

目录 1.前言 2.题目&#xff1a;奇怪的电梯 1.题目描述 2.输入格式 3.输出格式 4.输入输出样例 5.说明 6.题解 3.小结 1.前言 哈喽大家好啊&#xff0c;前一段时间小编去备战蓝桥杯所以博客的更新就暂停了几天&#xff0c;今天继续为大家带来题解分享&#xff0c;希望大…

网络管理实验二、SNMP服务与常用的网管命令

1 常用的网管命令 1.1 网络状态监视命令 包括以下命令&#xff1a;Ipconfig、ping、nslookup、dig、host ipconfig 作用&#xff1a;用来显示本机所有网卡的基本信息&#xff08;IP、掩码、网关、工作状态&#xff09;&#xff1b;用法&#xff1a;ipconfig展示&#xff1a;…