I 基础概念与环境准备

定义:

机器人操作系统第二代

理解:

机器人是个有不同模块组成的整体,每个模块有不同的软件控制代码和硬件设备。并且可能很复杂,所以需要一个用户和硬件之间的中间者来屏蔽底层细节,这就是ROS

ROS调度这些进程,任务,模块,使他们成为一个整体

ROS2提供:

1.丰富生态的社区

2.统一的通讯支持 DDS

3.各种实用轮子工具

4.强大的应用功能

ROS2使用的操作系统:

跨平台支持linux,windows,macos

但最好还是linux

使用ubuntu系统最好

ROS的安装

一个长期ubuntu系统对应一个ROS2版本,当前我使用的是jazzy对应24.04 支持到2029年

II 工程创建与工作空间 (Workspace)

创建 ROS 工程全流程

1.初始化依赖工具 非必要

TERMINAL / CODE 复制
rosdep init    一台电脑就用一次,已经执行过
TERMINAL / CODE 复制
rosdep update  规则变化或者网络波动需要再更新一次

连不上用rosdepc

2.创建工作空间(工程)

在合适地点创建一个文件夹,这就是工作空间

接着在工作空间里创建src文件夹,切换到里面

3.src内部创建功能包

有cpp版本和python版本,一个功能包就是类似于java里面的包,是一个库,导入别人的就是第三方库,自己写的就是自己的模块

CODE 复制
ros2 pkg create --build-type ament_python my_pkg
CODE 复制
 ros2 pkg create --build-type ament_cmake learning_pkg_c  

4.在功能包内写代码

5.安装第三方依赖

(1)之前的功能包是第三方的

(2)自己的包里面有一些别人封装好的功能

CODE 复制
cd ~/工程名
rosdep install --from-paths src --ignore-src -r -y
ENGINEERING ADVICE

注意:需要先回到父级目录下

6.编译项目

仍在父级目录下

CODE 复制
colcon build 

编译成功后就会生成另外三个部分,build log install

用Python的时候,第一次编译时加上 —symlink-install 参数(注意最前面是两个-),修改源码保存后如果没有新增文件可以不用再编译

以后修改自己的节点、URDF 或 launch 文件后,执行:

CODE 复制
python包
cd /home/asher/car_study
colcon build --symlink-install
source install/setup.bash

cpp包
cd /home/asher/car_cpp
colcon build --symlink-install
source install/setup.bash

7.加载自己的工作空间

CODE 复制
 source install/local_setup.sh 
echo " source ~/工程名/install/local_setup.sh" >> ~/.bashrc # 所有终端均生效

8.运行节点

CODE 复制
ros2 run my_pkg 节点名

工作空间目录架构

工程的所在地就是工作空间

一个工作空间由build log install src

功能包在src中

III 功能包机制 (Package)

CPP功能包

包含功能包的版权描述,和各种依赖的声明,功能是什么,谁创建了他 也就是一个各种杂项信息的结合

因为有依赖声明

rosdep就可以看这个自动解决依赖

告诉编译器如何组织cpp代码,如何编译等等

python功能包

一些基本配置

py需要把py可执行文件送到代码执行器的手里让他知道程序入口在哪,这个是setup文件里写的进入点入口

就是你在功能包里每写一个py文件都需要到这个setup里加上那个文件的运行入口,语法如下

前面是在外部启动这个节点程序要使用的名字 =这个程序入口,文件之间的层级关系用. 表示

CODE 复制
'node_helloworld       = learning_node.node_helloworld:main',

IV 节点架构 (Node) - 核心模块

其实就是一个程序或者说一个功能

节点是一个大功能的集合体,他负责管理内部的小功能,比如话题,服务等

这些小功能必须依存于节点,通过节点对象调用,存在

节点一般封装为类

为什么要有节点

完整的机器人系统可能在物理上不在一个整体

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

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

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

节点也可以比喻是一个一个的工人,分别完成不同的任务,他们有的在一线厂房工作,有的在后勤部门提供保障,他们互相可能并不认识,但却一起推动机器人这座“工厂”,完成更为复杂的任务。

创建节点流程

所以总体而言,节点的实现方式依然是这四个步骤,只不过编码方式做了一些改变而已。

›- 编程接口初始化

一般在main函数中

CODE 复制
 rclpy.init(args=args)  
›- 创建节点并初始化 要调用父类的初始化,再调用自己的初始化

这个一般在自己定义的节点类中写

CODE 复制
 super().__init__(name)   
›- 实现节点功能

在节点类中写

›- 销毁节点并关闭接口

main函数写

CODE 复制
  node.destroy_node()     
  rclpy.shutdown()                               # 销毁节点对象

spin循环

spin循环是在ros2退出才结束,这样节点的代码就可以一直执行

注意:

每次在节点里写代码后都需要重新编译,编译时,他会把可执行文件放到install里,我们在外面调用run时,是去install里找可执行文件的

节点常用命令

TERMINAL / CODE 复制
ros2 node list 节点列表
TERMINAL / CODE 复制
ros2 node info 节点名

这个注意一般是在另一台终端上启动并且查看的

V 话题通信 (Topic) - 异步发布订阅

节点之间传递数据的桥梁

话题的相关概念

就是话题中数据的数据结构或者说格式

是话题通信采用的一种机制,发送数据的对象称为发布者,他把数据封装为某一种类型发送到某个话题上,相当于把货物装到了这个道路上送出去,作为接受者的节点只需要订阅这个话题,相当于把道路连到那个话题道路上,这样就能接受这个和道路上奔流的数据并且解析,成功实现两个节点之间的通信

订阅者和发布者可以不唯一,是可以称为一张通信网的(同一个节点可以成为A话题的发布者也可以B话题的接受者)

采用基于DDS的异步通信

异步代表没有单独的时钟线控制发送者和接受者同步通信,而是双方约定好发送数据的速率,对方按照这个速率去读数据才能正确解析

这种异步通信模式适合一些周期性发布的数据,比如一些传感器数据,不需要感很强的实时性的这种

但对于指令这种实时性要求很高的,并且要求明确解析的数据就不适合异步通信了

话题通信数据的描述格式称之为消息,对应编程语言中数据结构的概念。比如这里的一个图像数据,就会包含图像的长宽像素值、每个像素的RGB等等,在ROS中都有标准定义。

定义数据类型等等

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

创建话题—伴生发布者与接受者

1.创建发布者对象,用构造函数,参1:消息的类型 参2:话题的名字 参3:缓冲队列长度(如果有发送失败的会在这里暂存)

CODE 复制
1. self.pub = self.create_publisher(String, "chatter", 10) 

2.创建定时器对象,定时执行回调函数(参数2) 参1:定时器间隔时间

回调函数就是嵌入式中的定时器函数,定时器到一个周期标志位置为1就会调用回调函数执行一些动作

CODE 复制
self.timer = self.create_timer(0.5, self.timer_callback)

3.填充回调函数

CODE 复制
 def timer_callback(self): 
  msg = String()  创建消息对象,消息对象在话题中流动
  msg.data = 'Hello World' 填充消息的数据
  self.pub.publish(msg)     把消息对象通过话题对象的发布方法发出去 
  self.get_logger().info('Publishing: "%s"' % msg.data)  # 输出日志信息,提示已经完成话题发布                                    

流程总结

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

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

接受者

创建接受者类(消息类型,话题名,回调函数,消息队列数)

类中进行初始化和属性的填写,功能的实现

填充回调函数

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

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

话题解耦合的操作,原始数据不要在发布者手里处理,而是发给接受者在底层处理

ros2库中的相机读取节点

CODE 复制
ros2 run usb_cam usb_cam_node_exe
›1. 读取设备摄像头:这些驱动节点底层通常基于 Linux 的 Video4Linux2 (V4L2) 接口,能够直接读取连接到系统的 USB 摄像头(如 /dev/video0 设备)的图像帧6。
›1. 发布 image_raw 消息:节点在获取到图像数据后,会将其封装为 ROS2 标准的 sensor_msgs/msg/Image 消息类型,并发布到相应的话题上

常用指令

$ 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> # 发布话题消息

局限性

话题通信的特性是单向传输,适合周期性的数据传递,

VI 服务通信 (Service) - 同步请求响应

定义:

发送请求,立刻得到回答,高实时性

客户端/服务器模型

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

客户端不唯一,服务端唯一,为多个客户端提供服务

客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信

数据的结构和类型需要在srv文件中定义

接口类型

就是数据的数据类型 可以是基本数据类型,也可以是对象

创建客户端过程

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])

main函数会把终端启动时后面的参数传入main函数,这里的sys.argv就是获取main函数的参数

self.future = self.client.call_async(self.request) # 异步方式发送服务请求

这里的异步方式就是发完之后不会等待回复

main函数

CODE 复制
 node = adderClient("service_adder_client")    
  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                                # 创建ROS2节点对象并进行初始化

创建客服端请求

类似于订阅者

代码:

ENGINEERING ADVICE

注意:服务端不需要调用显式回复函数,他接收到请求,做完服务会自动返回回复给客户端,客户端那边直接用response就行

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 a: %d b: %d' % (request.a, request.b)) # 输出日志信息,提示已经完成加法求和计算

return response # 反馈应答信息

服务相关命令行

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

VII 数据接口 (Interface) - 通信标准结构

(数据的格式和类型)

定义:就是各种自定义的数据如何封装成数据类型

不用关心底层细节,只需要按照接口文档使用数据就行,类似的,键盘控制我们也可以安装一个ROS包,如何实现的呢?不用关心,反正它发布出来的肯定是线速度和角速度。

接口的组织结构

这个也是一个软件包

如果是服务里面创建name.srv文件

如果是话题创建msg文件

如果是行为创建action文件

里面的内容就是定义的数据的格式

用的时候from learning_interface.srv import AddTwoInts # 自定义的服务接口

导入这个格式并命名为AddTwoInts 这样这个就相当于自创了一个数据类型,类似string这种,可以传入一些需要数据类型的函数中

语言无关

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

使用他人包

使用他人的功能包时会用到一些接口,这些他们都会提供,你用的时候直接提取数据就行,比如cam包,直接获取image数据就能看图像

接口命令

接口命令的常用操作如下:

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

VIII 动作机制 (Action) - 完整流程过程管理

定义:

也是客户端,服务端模型

目标,反馈,结果

底层是基于两个服务,一个话题进行的

这种通信机制的目的就是便于对机器人某一完整行为的流程进行管理。

就像装了一个进度条,我们可以随时把控进度

一对多通信

同步通信

ORIGINAL CITATION & REFERENCES

资料与原始文档