ROS2基础知识

826 阅读3分钟

version 0.1

参考

book.guyuehome.com/

ROS (Robot Operating System) 和 ROS 2 (Robot Operating System 2) 是两个由 Open Robotics 开发的机器人操作系统。尽管名称中带有“操作系统”,但实际上它们并不是传统意义上的操作系统,而是提供了一系列工具、库和约定来帮助开发者创建机器人软件。

与ROS的差别

  1. 去除了master,改为了分布式DDS,只有一个抽象的中间件接口,通过该接口进行传输数据

image.png

  1. 内置了安全功能,能够保证通信的加密

  2. 多语言(C++, Python, Java)多操作系统(Linux,Windows,Mac,RTOS)支持

  3. 采用了新的管理工具ament、colcon;ROS1的编译命令是catkin_make,而ROS2的编译命令使用colcon build命令

ROS2安装

设置UTF-8

必须支持UTF-8编码

添加对应的源

sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg 
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

换源 mirrors.tuna.tsinghua.edu.cn/help/ros2/

安装

sudo apt update
sudo apt upgrade
sudo apt install ros-foxy-desktop python3-argcomplete
sudo apt install python3-colcon-common-extensions

加入环境变量

source /opt/ros/foxy/setup.bash
echo "source /opt/ros/foxy/setup.bash" >> ~/.bashrc

工作空间

.
├── build
├── install
├── log
└── src

ROS系统中一个典型的工作空间结构如上所示,里边会有四个子目录。

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

总体来讲,这四个空间的文件夹,我们绝大部分操作都是在src中进行的,编译成功后,就会执行install里边的结果,build和log两个文件夹用的很少。

我们只需要创建src文件夹,并使用colcon build即可;就自动生成build,install,log

mkdir -p ~/{workspace}/src
cd ~/{workspace}
colcon build
source install/setup.sh # 仅在当前终端生效
echo " source ~/{workspace}/install/setup.sh" >> ~/.bashrc # 所有终端均生效

功能包

工作空间下的src又可以创建功能包

ros2 pkg create [-h] [--package-format {2,3}]
               [--description DESCRIPTION] [--license LICENSE]
               [--destination-directory DESTINATION_DIRECTORY]
               [--build-type {cmake,ament_cmake,ament_python}]
               [--dependencies DEPENDENCIES [DEPENDENCIES ...]]
               [--maintainer-email MAINTAINER_EMAIL]
               [--maintainer-name MAINTAINER_NAME]
               [--node-name NODE_NAME] [--library-name LIBRARY_NAME]
               package_name
  • pkg:表示功能包相关的功能;
  • create:表示创建功能包;
  • package_name:新建功能包的名字。
  • build-type:表示新创建的功能包是C++还是Python的,如果使用C++或者C,那这里就跟ament_cmake,如果使用Python,就跟ament_python;
  • dependencies:表示功能包的依赖项,C++功能包需包含rclcpp,Python功能包需包含rclpy ,还有其它需要的依赖;
  • node-name:可执行程序的名称,会自动生成对应的源文件并生成配置文件;
# 创建C++功能包
ros2 pkg create --build-type ament_cmake --dependencies rclcpp --node-name helloworld pkg_helloworld_cpp
# 创建Python功能包
ros2 pkg create --build-type ament_python --dependencies rclpy --node-name helloworld pkg_helloworld_py
├── pkg_helloworld_cpp
│   ├── CMakeLists.txt 编译规则
│   ├── include  头文件
│   ├── src 源文件
|   ├── msg 消息接口文件夹
|   ├── srv 服务接口文件夹
|   ├── package.xml  描述文件
│   └── action 动作接口文件夹
└── pkg_helloworld_py
    ├── package.xml  描述文件
    ├── pkg_helloworld_py  源文件目录
    ├── resource  资源目录
    ├── setup.cfg 功能包配置文件
    ├── setup.py  解释规则
    └── test  测试文件目录

节点

我们将机器人的每个功能点称为一个节点,一般情况下每个节点都对应某一单一的功能模块,一个完整的机器人系统可能由许多协同工作的节点组成

订阅

就是其他编程语言里的消息队列

创建节点流程

  1. 编程接口初始化
  2. 创建节点并初始化
  3. 实现节点功能
  4. 销毁节点并关闭接口

例子 topic-pub-sub

image.png

.
└── topic_demo
    ├── package.xml
    ├── resource
    │   └── topic_demo
    ├── setup.cfg
    ├── setup.py
    ├── test
    │   ├── test_copyright.py
    │   ├── test_flake8.py
    │   └── test_pep257.py
    └── topic_demo
        ├── __init__.py
        ├── pub.py
        └── sub.py

创建文件目录

cd ./dev_ws/src
ros2 pkg create --build-type ament_python --dependencies rclpy --node-name pub  topic_demo
cd ./topic_demo/topic_demo
touch sub.py

pub.py

# pub.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Topic_Pub(Node):
    def __init__(self,name):
        super().__init__(name)
        #创建一个发布者,使用create_publisher的函数,传入的参数分别是:
        #话题数据类型、话题名称、保存消息的队列长度
        self.pub = self.create_publisher(String,"/topic_demo",1)
        #创建一个定时器,间隔1s进入中断处理函数,传入的参数分别是:
        #中断函数执行的间隔时间,中断处理函数
        self.timer = self.create_timer(1,self.pub_msg)
    #定义中断处理函数
    def pub_msg(self):
        msg = String() #创建一个String类型的变量msg
        msg.data = input("input: ") #给msg里边的data赋值
        self.pub.publish(msg) #发布话题数据
def main():
    rclpy.init() #初始化
    pub_demo = Topic_Pub("publisher_node") #创建Topic_Pub类对象,传入的参数就是节点的名字
    rclpy.spin(pub_demo) #执行rclpy.spin函数,里边传入一个参数,参数是刚才创建好的Topic_Pub类对象
    pub_demo.destroy_node() #销毁节点对象
    rclpy.shutdown() #关闭ROS2 Python接口

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

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

sub.py

# sub.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Topic_Sub(Node):
    def __init__(self,name):
        super().__init__(name)
        #创建订阅者使用的是create_subscription,传入的参数分别是:话题数据类型,话题名称,回调函数名称,队列长度
        self.sub = self.create_subscription(String,"/topic_demo",self.sub_callback,1)
        # 回调函数执行程序:打印接收的到信息
    def sub_callback(self,msg):
        print(msg.data)
def main():
    rclpy.init() #ROS2 Python接口初始化
    sub_demo = Topic_Sub("subscriber_node") # 创建对象并进行初始化
    rclpy.spin(sub_demo)
    sub_demo.destroy_node() #销毁节点对象
    rclpy.shutdown()

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

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

修改 setup.py

entry_points={
    'console_scripts': [
        'sub = topic_demo.sub:main',
        'pub = topic_demo.pub:main'
    ],
},
cd ~/dev_ws
colcon build  # 构建
source install/setup.bash  # 设置环境变量
ros2 run topic_demo sub  # 启动sub节点
ros2 run topic_demo pub  # 启动pub节点

image.png

由此可见,是广播模式

服务

image.png

所谓的服务,就类似于我们平时的浏览器-服务器,是一问一答的模式;更像http短链接

具体的编程模式依然如同订阅模式

  1. 编程接口初始化
  2. 初始化节点
  3. 初始化client/server
  4. 发送消息等待响应/等待接收消息并处理
  5. 销毁节点
  6. 销毁程序

动作

动作是一种更为综合的通信方式,更像socket通信

  1. 客户端请求动作
  2. 服务器done
  3. 客户端请求数据
  4. while 服务器返回
  5. 服务器done

通信模型

image.png

自定义接口

topic

string name
int32 age
float64 height

server

int32 num1
int32 num2
---
int32 sum

action

int32 goal
---
int32 sum
---
int32 feedback

编译方式和节点一样

创建动作描述

ros2 pkg create --build-type ament_cmake pkg_interfaces

Progress.action

int64 num   # 目标 input
---
int64 sum   # 结果 result
---
float64 progress  # 反馈 feedback

在package.xml中需要添加一些依赖包,具体内容如下:

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>action_msgs</depend>
<member_of_group>rosidl_interface_packages</member_of_group>

在CMakeLists.txt 中添加如下配置:

find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
    "action/Progress.action"
)
colcon build

然后就可以直接使用了,

from pkg_interfaces.action import Progress

参数

类似C++编程中的全局变量,可以便于在多个程序中共享某些数据,参数是ROS机器人系统中的全局字典,可以运行多个节点中共享数据。;所谓字典,就类似于Java中的map,是一种键值对

ros2 param list # 查询参数
ros2 param describe [node] [param] # 查看某个参数的描述信息
ros2 param get turtlesim background_b # 查询某个参数的值
ros2 param set turtlesim background_b 10 # 修改某个参数的值
ros2 param dump turtlesim >> turtlesim.yaml # 将某个节点的参数保存到参数文件中
ros2 param load turtlesim turtlesim.yaml # 一次性加载某一个文件中的所有参数

例如,小车在运行时会有一个速度v的变量,我们可以将v设置为全局变量,这样就可以通过修改全局变量v来控制小车速度;当然,通过topic,service也可以实现这类功能,只是编码难度不同

分布式通信

多机通讯即分布式通信是指可以通过网络在不同主机之间实现数据交互的一种通信策略。

ROS2本身是一个分布式通信框架,可以很方便的实现不同设备之间的通信,ROS2所基于的中间件是DDS,当处于同一网络中时,通过DDS的域ID机制(ROS_DOMAIN_ID)可以实现分布式通信,大致流程是:在启动节点之前,可以设置域ID的值,不同节点如果域ID相同,那么可以自由发现并通信,反之,如果域ID值不同,则不能实现。默认情况下,所有节点启动时所使用的域ID为0,换言之,只要保证在同一网络,你不需要做任何配置,不同ROS2设备上的不同节点即可实现分布式通信。

可在 ~/.bashrc 下通过export ROS_DOMAIN_ID=<your_domain_id>来设置ID

分布式通信的应用场景是较为广泛的,无人车编队、无人机编队、远程控制等等, 这些数据的交互都依赖于分布式通信。

应用:由于存在机器人运算平台较弱,故可以通过分布式通信,将运算任务转移至其他运算能力更强的平台来处理,机器人只负责使用运算结果

  1. 建议ROS_DOMAIN_ID的取值在[0,101] 之间,包含0和101;
  2. 每个域ID内的节点总数是有限制的,需要小于等于120个;
  3. 如果域ID为101,那么该域的节点总数需要小于等于54个。

image.png

image.png

在设置好后,我们可以将之前所写的程序的client移入机器人,service移入PC,两者依然可以通信

元功能包

类似于maven的pom父包,仅用于整合模块 package.xml

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd"
schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
    <name>pkg_metapackage</name>
    <version>0.0.0</version>
    <description>TODO: Package description</description>
    <maintainer email="">root</maintainer>
    <license>TODO: License declaration</license>
    <buildtool_depend>ament_cmake</buildtool_depend>

    <exec_depend>pkg_interfaces</exec_depend>
    <exec_depend>pkg_helloworld_py</exec_depend>
    <exec_depend>pkg_topic</exec_depend>
    <exec_depend>pkg_service</exec_depend>
    <exec_depend>pkg_action</exec_depend>
    <exec_depend>pkg_param</exec_depend>

    <test_depend>ament_lint_auto</test_depend>
    <test_depend>ament_lint_common</test_depend>
    <export>
        <build_type>ament_cmake</build_type>
    </export>
</package>

......