0%

ROS2备忘录

安装

ubuntu 20.04

1
sudo apt install ros-rolling-desktop

http://docs.ros.org/en/rolling/Installation/Ubuntu-Install-Debians.html#

配置环境

  • “workspace”是一个ROS术语,表示在系统上使用ROS2进行开发的位置。
  • 核心ROS工作区被称为”underlay“,本地ROS工作区称为 ”overlays“

环境设置脚本自启动

1
2
echo "source /opt/ros/rolling/setup.bash" >> ~/.bashrc
printenv | grep -i ROS

基础概念

Node

ROS图(ROS graph)是一个由ROS 2元素组成的网络,在同一时间一起处理数据。图中包括所有的可执行文件和它们之间的联系。

ROS中的每个节点(ROS Node)应该只负责单一模块化的用途(例如,一个节点负责控制车轮马达,一个节点负责控制激光测距仪等等)。每个节点可以通过主题(topics)、服务(services)、行动(actions)或参数(parameters)向其他节点发送和接收数据。

../_images/Nodes-TopicandService.gif

一个完整的机器人系统由许多协同工作的节点组成。在ROS2中,单个可执行文件(C++程序、Python程序等)可以包含一个或多个节点。

命令

  • ros2 run <package_name> <executable_name>:从包启动可执行文件。

  • ros2 node list:显示所有运行节点的名称,当您想要与一个节点交互时,或者当您的系统运行许多节点并且需要跟踪它们时,这尤其有用。

  • 使用Remapping可以指定节点属性(如节点名称、主题名称、服务名称等)

    例:ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle

  • ros2 node info <node_name> :知道了节点的名称,可以通过该命令访问有关节点的更多信息

Topics

ROS2将复杂系统分解为许多模块化节点。主题(Topics)是ROS图的一个重要元素,充当节点交换消息的总线。

../../_images/Topic-SinglePublisherandSingleSubscriber.gif

节点可以将数据发布到任意数量的主题,同时可以订阅任意数量的主题。主题是在节点之间以及系统不同部分之间移动数据的主要方式之一。也就是说,主题可以不只是点对点的交流,它可以是一对多、多对一或多对多。

../../_images/Topic-MultiplePublisherandMultipleSubscriber.gif

工具

我们可以使用rqt_graph来可视化不断变化的节点和主题,以及它们之间的连接。

../../_images/rqt_graph.png

上图描述了/turtlesim节点和/teleop_turtle节点如何通过主题相互通信。/teleop_turtle节点将数据发布到/turtle1/cmd_vel主题中(输入用于移动海龟的按键),并且/turtlesim节点订阅该主题以接收数据。

命令

  • ros2 topic list:返回系统中当前活动的所有主题的列表

  • ros2 topic list -t:将返回相同的主题列表,返回结果括号中的是主题类型。

  • ros2 topic echo <topic_name> :查看主题上正在发布的数据

  • ros2 topic info <topic_name>:返回主题信息(有多少个订阅者,有多少个发布者)

  • ros2 interface show <type>:节点使用消息通过主题发送数据。发布者和订阅者必须发送和接收相同类型的消息才能进行通信。如果主题/turtle1/cmd_vel的类型是geometry_msgs/msg/Twist,这意味着在geometry_msgs包中有一个名为Twist的消息。ros2 interface show <msg type>可以查看该类型的细节,假设得到的类型细节为:

    1
    2
    3
    4
    5
    6
    7
    8
    Vector3  linear
    float64 x
    float64 y
    float64 z
    Vector3 angular
    float64 x
    float64 y
    float64 z

    这说明/turtlesim节点需要一条消息,该消息包含两个向量,即linear向量和angular向量,每个向量包含三个元素。

  • ros2 topic pub <topic_name> <msg_type> '<args>':使用该命令直接将数据发布到主题,前提是数据的具体结构。需要注意的是,args需要符合YAML语法规范,例:

    1
    ros2 topic pub --once  /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}}"
  • ros2 topic hz :查看数据发布的速率

Services

服务(services)是ROS图中节点的另一种通信方法。服务基于呼叫和响应(call-and-response)模型,而不是发布者-订阅者(publisher-subscriber)模型。虽然主题允许节点订阅数据流并获得持续更新,但服务仅在客户端专门调用时提供数据。

../../_images/Service-SingleServiceClient.gif

../../_images/Service-MultipleServiceClient.gif

命令

  • ros2 service list:返回系统中当前活动的所有服务的列表
  • ros2 service type <service_name> :服务类型的定义与主题类型类似,只是服务类型有两部分:一部分用于请求,另一部分用于响应。假如一个服务的类型是std_srvs/srv/Empty,这表示服务调用在发出请求时不发送数据,在接收响应时不接收数据。
  • ros2 service find <service_type>:查找特定类型的所有服务
  • ros2 interface show <service_type_name> :返回类型的具体结构,返回结果的---将请求结构(上面)与响应结构(下面)分开。
  • ros2 service call <service_name> <service_type> <arguments>:调用服务

Parameters

参数(parameters)是节点的配置值。节点可以将参数存储为整数、浮点、布尔、字符串和列表。在ROS2中,每个节点维护自己的参数。所有参数都可动态重新配置,并基于ROS 2服务构建。

命令

  • ros2 param list :查看参数列表

  • ros2 param get <node_name> <parameter_name>:查看参数类型和当前值

  • ros2 param set <node_name> <parameter_name> <value>:设置参数值

  • ros2 param dump > dumpfile:查看节点的所有当前参数值

  • ros2 param load <node_name> <parameter_file>:将参数从文件加载到当前运行的节点

    只读参数只能在启动前修改,启动后修改有可能会出现问题(比如一些QOS参数)

  • ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>:在启动时加载参数

Actions

Actions是ROS 2中的通信类型之一,用于长时间运行的任务。它们由三部分组成:目标(goal)、反馈(feedback)和结果(result)

../_images/Action-SingleActionClient.gif

行动建立在主题和服务之上,功能与服务类似,只是可以取消操作。它们还提供稳定的反馈,而不是像服务一样只返回单一响应。

Actions使用client-server模型,类似于pubsub模型(在主题教程中介绍)。“action client”节点向“action server”节点发送目标,后者确认目标并返回反馈流和结果。

命令

  • ros2 action list -t

  • ros2 action info

  • ros2 interface show:返回结果被---分为三个部分,分别是目标请求的结构、结果的结构、反馈的结构。

  • ros2 action send_goal <action_name> <action_type> <values> --feedback :发生Action中的目标

工作区

工作区(workspace)是包含ROS2包的目录。

  • source ROS2环境:source /opt/ros/rolling/setup.bash

  • 创建目录:最好是为每个新的工作区创建新目录,并将所有的包都放置在src文件夹

    src文件夹执行:git clone https://github.com/ros/ros_tutorials.git -b rolling-devel

  • 在构建工作区之前,您需要解析包依赖关系:rosdep install -i --from-path src --rosdistro rolling -y

    找不到rosdep命令 的解决方法:

    1
    >apt install python3-rosdep2

    rosdep update 失败的解决方法:

    1. 使用rosdepc:本文之后,世上再无rosdep更新失败问题!如果有….小鱼就… - 古月居 (guyuehome.com)
    2. 使用github proxy:ROS安装过程中如何解决 rosdep update 命令出现错误 - 知乎 (zhihu.com)
  • 使用colcon构建工作空间:colcon buildbuild完成后会生成三个文件夹,buildinstalllog,其中install目录是您的工作区的安装文件所在的位置,您可以使用它来source overlay。

    • 找不到colcon
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    sudo apt update && sudo apt install -y \
    build-essential \
    cmake \
    git \
    python3-colcon-common-extensions \
    python3-flake8 \
    python3-pip \
    python3-pytest-cov \
    python3-rosdep \
    python3-setuptools \
    python3-vcstool \
    wget
    # install some pip packages needed for testing
    python3 -m pip install -U \
    flake8-blind-except \
    flake8-builtins \
    flake8-class-newline \
    flake8-comprehensions \
    flake8-deprecated \
    flake8-docstrings \
    flake8-import-order \
    flake8-quotes \
    pytest-repeat \
    pytest-rerunfailures \
    pytest
    • build 失败的解决办法:

    python版本的问题,将/usr/bin中的python3.5执行文件改成其他文件名,只留下python3.8执行文件

  • source overlay(本地工作区):. install/local_setup.bash

    在source本地工作区之前,需要打开一个新的终端,与构建工作区的终端分开,这一点非常重要,不然有可能会产生复杂的问题

  • 修改本地工作区的代码:

    1. ~/dev_ws/src/ros_tutorials/turtlesim/src/turtle_frame.cpp中52行的setWindowTitle("TurtleSim");修改为setWindowTitle("MyTurtleSim");
    2. colcon build
    3. . install/local_setup.bashros2 run turtlesim turtlesim_node

ROS包

包(package)可以被认为是你的ROS 2代码的容器。如果你希望能够安装你的代码或与他人分享,那么你就需要把它组织在一个包里。通过包,你可以发布你的ROS 2工作,并允许其他人轻松构建和使用它。

ROS 2的软件包创建使用ament作为其构建系统,colcon作为其构建工具,你可以使用官方支持的CMake或Python来创建一个软件包。除了这些,也存在其他的构建方式。

Ament是catkin编译工具的优化迭代版本。

colcon是一个构建软件包集合的命令行工具,是ros构建工具catkin_make, catkin_make_isolated, catkin_tools 和ament_tools的迭代版本。

Python构建软件包需要:

  • package.xml文件,包含关于包的元信息。
  • setup.py,包含如何安装软件包的说明
  • setup.cfg 在软件包有可执行文件时是必需的,这样 ros2 run 可以找到它们。
  • /<package_name> 一个与你的软件包同名的目录,被ROS 2工具用来寻找你的软件包,包含__init__.py

最简单的Python包文件结构:

1
2
3
4
my_package/
setup.py
package.xml
resource/my_package

CMake构建软件包需要:

  • package.xml文件,包含关于包的元信息。

  • CMakeLists.txt文件,描述了如何构建包内的代码

最简单的的C/C++包文件结构:

1
2
3
4
my_package/
setup.py
package.xml
resource/my_package

一个工作区可以包含你想要的任何数量的包,每个包都在自己的文件夹里。你也可以在一个工作区中拥有不同构建类型的包(CMake、Python等),但是不能有嵌套的包。最好的做法是在你的工作区里有一个src文件夹,并在那里创建你的包。这样可以保持工作区顶层的干净。

一个典型的工作空间可能看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
workspace_folder/
src/
package_1/
CMakeLists.txt
package.xml

package_2/
setup.py
package.xml
resource/package_2
...
package_n/
CMakeLists.txt
package.xml

在ROS 2中创建一个新包的命令语法是:

1
ros2 pkg create --build-type ament_cmake <package_name>

或者

1
ros2 pkg create --build-type ament_python <package_name>

例子:

1
ros2 pkg create --build-type ament_cmake --node-name my_node my_package

把软件包放在工作区是一件有意义的事,因为你可以通过在工作区根目录下运行colcon build来一次性构建所有软件包,而不用单独构建每个包。

1
colcon build --packages-select my_package

build失败,原因是email和maintainer包含反斜杆,去掉就行

尝试运行,ros2 run my_package my_node会看到输出。修改my_node.cpp,重新编译运行,可以看到不同的输出。

pubsub模型

C++版本

Python版本

创建包

src文件下执行:

1
ros2 pkg create --build-type ament_python py_pubsub

发布Node

src/py_pubsub/py_pubsub目录下添加文件publisher_member_function.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import rclpy
from rclpy.node import Node


# import内置的字符串消息类型,Node使用可以该类型来结构化它在Topic上传递的数据。
from std_msgs.msg import String

# MinimalPublisher继承自Node类型
class MinimalPublisher(Node):

def __init__(self):
# 指定node名称是minimal_publisher
super().__init__('minimal_publisher')
# 声明该节点在一个名为topic的主题上发布String类型的消息,并且 "队列大小 "为10。
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
# 创建了一个定时器,它的回调每0.5秒执行一次
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0

# timer_callback函数创建一个附加有计数器值的消息,并在控制台打印该值。
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info(': "%s"' % msg.data)
self.i += 1



def main(args=None):
# 初始化 rclpy 库
rclpy.init(args=args)

# 创建节点
minimal_publisher = MinimalPublisher()

# "spin"节点,使其回调被调用。
#
rclpy.spin(minimal_publisher)

# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_publisher.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

spin与ROS线程调度:ROS publisher queue, subscriber queue, callback queue and spinner threads tutorial | Level Up Coding (gitconnected.com)

添加依赖

package.xml中添加依赖:

1
2
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>

添加入口点(entry point)

打开setup.py,将信息修改为package.xml中的信息。并修改以下字段:

1
2
3
4
5
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
],
},

检查setup.cfg

一般来说,setup.cfg的文件内容是:

1
2
3
4
[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub

含义是告诉 setuptools 要把你的可执行文件放在 lib 中,因为 ros2 run 会在那里寻找可执行文件。

订阅Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalSubscriber(Node):

def __init__(self):
super().__init__('minimal_subscriber')
# 订阅者的代码不需要定时器,因为一旦收到消息,它的回调就会被调用。
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning

# 回调函数简单地将消息的数据打印到控制台。
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)


def main(args=None):
rclpy.init(args=args)

minimal_subscriber = MinimalSubscriber()

rclpy.spin(minimal_subscriber)

# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_subscriber.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

setup.py添加入口点:

1
'listener = py_pubsub.subscriber_member_function:main',

编译运行

1
2
3
4
5
6
7
8
9
10
11
12
13
#检查依赖
rosdep install -i --from-path src --rosdistro rolling -y
#编译安装
colcon build --packages-select py_pubsub

#运行
#terminal1
. install/setup.bash
ros2 run py_pubsub talker

#terminal2
. install/setup.bash
ros2 run py_pubsub listener

CS模型

C++版本

请求和响应的结构由一个.srv文件决定。

创建包

1
ros2 pkg create --build-type ament_cmake cpp_srvcli --dependencies rclcpp example_interfaces

参数--dependencies将自动在package.xmlCMakeLists.txt中添加必要的依赖关系行。

example_interfaces包中有我们将需要的.srv文件的包。

1
2
3
4
int64 a
int64 b
---
int64 sum

服务端Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <memory>


// add函数将请求中的两个整数相加,并将sum交给响应,同时在控制台打印信息。
void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
request->a, request->b);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
// 初始化ROS 2 C++客户端库
rclcpp::init(argc, argv);
// 创建一个名为add_two_ints_server的节点
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
// 为该节点创建一个名为add_two_ints的服务,并与 add 方法绑定
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");
// spin节点,使服务可用。

rclcpp::spin(node);
rclcpp::shutdown();
}

添加入口

add_executable宏会生成一个可以用ros2 run来运行的执行文件,在CMakeLists.txt中添加以下代码块,以创建一个名为server的可执行文件。

1
2
3
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp example_interfaces)

为了让ros2运行能够找到可执行文件,需要在文件末尾添加以下几行,就在ament_package()之前。

客户端Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);

if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
return 1;
}

std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);

while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}

auto result = client->async_send_request(request);
// Wait for the result.
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
}

rclcpp::shutdown();
return 0;
}

一样需要添加入口

编译运行

1
2
3
4
5
6
7
colcon build --packages-select cpp_srvcli

. install/setup.bash
ros2 run cpp_srvcli server

. install/setup.bash
ros2 run cpp_srvcli client 2 3

Python版本

自定义的msg和srv

创建包

1
ros2 pkg create --build-type ament_cmake tutorial_interfaces

注意必须是一个CMake包。

好的做法是将.msg.srv文件放在软件包内各自的目录中。

创建自定义的definitions

注意msgsrvsrc文件夹在同一层级

  • Num.msg

    1
    int64 num
  • AddThreeInts.srv

    1
    2
    3
    4
    5
    int64 a
    int64 b
    int64 c
    ---
    int64 sum
  • CMakeLists.txt:将定义的接口转换为特定语言的代码(如C++和Python),以便它们可以在这些语言中使用,需要添加以下代码

    1
    2
    3
    4
    5
    6
    find_package(rosidl_default_generators REQUIRED)

    rosidl_generate_interfaces(${PROJECT_NAME}
    "msg/Num.msg"
    "srv/AddThreeInts.srv"
    )
  • package.xml:接口依赖rosidl_default_generators来生成特定语言的代码,你需要声明对它的依赖性。

    1
    2
    3
    4
    5
    <build_depend>rosidl_default_generators</build_depend>

    <exec_depend>rosidl_default_runtime</exec_depend>

    <member_of_group>rosidl_interface_packages</member_of_group>

编译查看

1
2
3
4
colcon build --packages-select tutorial_interfaces

. install/setup.bash
ros2 interface show tutorial_interfaces/msg/Num

使用

C++

  • 源码

    1
    #include "tutorial_interfaces/msg/num.hpp"                      # CHANGE
  • CMakeLists.txt

    1
    2
    3
    find_package(tutorial_interfaces REQUIRED)                      # CHANGE
    ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE
    ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE
  • package.xml

    1
    <depend>tutorial_interfaces</depend>

Python

  • 源码

    1
    from tutorial_interfaces.msg import Num                        # CHANGE
  • package.xml

    1
    <exec_depend>tutorial_interfaces</exec_depend>

拓展ROS 2的接口(interface)

你可以在一个新的接口定义中使用一个现有的接口定义。

ROS Launch

ROS2 启动系统

ROS 2中的启动系统负责帮助用户描述其系统的配置,然后按照描述执行。系统的配置包括运行什么程序,在哪里运行,向它们传递什么参数,以及ROS特定的约定,通过给它们各自不同的配置,使整个系统中的组件易于重复使用。它还负责监控启动的进程的状态,并报告和/或对这些进程的状态变化做出反应。

用Python编写的启动文件可以启动和停止不同的节点,也可以触发各种事件并对其采取行动。提供这个框架的包是 launch_ros,它在下面使用非ROS专用的启动框架。

编写ROS 2启动文件

如果你还没有,请确保你浏览了关于如何创建ROS 2软件包的快速入门教程。在ROS 2中创建启动文件的一种方法是使用Python文件,这些文件由ROS 2 CLI工具ros2 launch来执行。我们首先使用ros2 pkg create <pkg-name> --dependencies [deps] 在我们的工作区创建一个ROS 2软件包,并创建一个新的启动目录。

C++ 包

如果你正在创建一个C++包,我们将只调整CMakeLists.txt文件,添加:

1
2
3
4
5
# Install launch files.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)

到文件的末尾(但在ament_package()之前)。

编写启动文件

在你的启动目录中,创建一个后缀为.launch.py的新启动文件。例如:my_script.launch.py

你的启动文件应该定义generate_launch_description(),它返回一个 launch.LaunchDescription(),供 ros2 launch使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import launch
import launch.actions
import launch.substitutions
import launch_ros.actions


def generate_launch_description():
return launch.LaunchDescription([
launch.actions.DeclareLaunchArgument(
'node_prefix',
default_value=[launch.substitutions.EnvironmentVariable('USER'), '_'],
description='Prefix for node names'),
launch_ros.actions.Node(
package='demo_nodes_cpp', node_executable='talker', output='screen',
node_name=[launch.substitutions.LaunchConfiguration('node_prefix'), 'talker']),
])

使用

1
ros2 launch my_package script.launch.py