跟ROS相同,ROS 2也是建议创建一个工作空间,方便管理同一个项目的packages,而且也是将package源文件都放入src文件夹中。
创建工作空间:
mkdir -p ~/colcon_ws/src
cd colcon_ws/src
现在我们先关注 colcon 的编译过程,所以 package 源文件就先借用官网的。
git clone https://github.com/ros2/examples
git checkout $ROS_DISTRO # 切换到与本机版本对应的 branch 上
# 电脑上要是有两个ROS系统,需要先 source /opt/ros/eloquent/setup.bash
用 colcon 编译下载的 package
cd ..
colcon build
编译后,产生了 build, install, log 三个新文件夹。
如果要单独编译某一个 package,可以用如下命令:
colcon build --packages-select PACKAGE_NAME
如果不希望编译某一个 package,可以在该 package 中创建名为 COLCON_IGNORE 的空文件,colcon 就会忽略掉该 package,不但不编译,连 colcon list 都不显示,这个 package 对 colcon 就是透明的。
完成编译之后,要 source 一下 setup.bash 文件,确保系统能够找到当前编译生成的可执行文件和库:
source install/setup.bash
或者将 source 命令放入 .bashrc 文件,这样每次打开 terminal 就可以自动加载路径信息:
echo "source ~/colcon_ws/install/setup.bash" >> ~/.bashrc
我自己的.bashrc文件修改如下(针对电脑双ROS):
这里为老版本,使用起来比较方便,但是在部分程序的 build 等步骤会杂糅很多不需要的东西:
# version 1.0
CUR_PATH=$(pwd | cut -d / -f 4)
if [[ "$CUR_PATH" == "colcon_ws" ]];then
source /opt/ros/eloquent/setup.bash
source ~/colcon_ws/install/setup.bash
echo 'ROS 2 sourced'
else
source /opt/ros/melodic/setup.bash
source ~/catkin_ws/devel/setup.bash
echo 'ROS sourced'
fi
因此修正后的代码为:
# version 2.0
alias ros1_="source /opt/ros/melodic/setup.bash && source ~/catkin_ws/devel/setup.bash && echo 'ROS sourced' "
alias ros2_="source /opt/ros/eloquent/setup.bash && source ~/colcon_ws/install/setup.bash && echo 'ROS 2 sourced'"
首先启动一个 publisher
ros2 run examples_rclcpp_minimal_publisher publisher_member_function
再启动一个 subscriber
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function
终端显示如下:
这里与 ROS 最大的区别是 不需要启动 ROS master 节点,即不需要类似 roscore 的命令。ROS 2 是真正的分布式系统,不需要中心节点,这样系统的鲁棒性更强,不会因为中心节点失效而影响整个系统。
ROS 2 根据命令的作用对象划分成多个类别,其中常用的几个类别:
Commands:
launch Run a launch file
node Various node related sub-commands
param Various param related sub-commands
pkg Various package related sub-commands
run Run a package specific executable
service Various service related sub-commands
srv Various srv related sub-commands
topic Various topic related sub-commands
在调用时都是采用如下命令格式:
ros2 COMMAND ...
我们可以对比 ROS 和 ROS 2 中的几个命令,应该很容易找到其中的规律
运行 ROS node
- ROS:rosrun
- ROS 2:ros2 run
查看当前运行的 node
- ROS:rosrun
- ROS 2:rosrun
命令 ros2 run 从一个 package 中情动可执行文件:
ros2 run
ros2 node list 将会展示你所有运行着的节点名称:
ros2 node list
Remapping 允许你重新分配默认节点属性,像节点名, topic 名,service 名等等;
重新分配节点名 /turtlesim 的方式如下:
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
此时会出现两个节点名:
/turtlesim
/my_turtle
node info 的方式可以获取节点的更多信息:
ros2 node info
topic 不一定是一对一的通信,它也可以是一对多、多对一或者多对多的。
rqt_graph 用来可视化正在改变的 node 和 topic。
要运行 rqt_graph,需打开一个新终端并输入命令:
rqt_graph
在新的终端下运行 ros2 topic list 命令会返回在系统内所有当前活跃的 topic:
ros2 topic list
ros2 topic list -t 会返回相同的 topic 列表,这时候 topic 会额外显示其类型:
ros2 topic list -t
差异如下:
/parameter_events
/rosout
/turtle1/cmd_vel
/parameter_events [rcl_interfaces/msg/ParameterEvent]
/rosout [rcl_interfaces/msg/Log]
/turtle1/cmd_vel [geometry_msgs/msg/Twist]
浏览数据在话题上发布:
ros2 topic echo
浏览数据在话题上发布:
ros2 topic echo
该命令可以用来直接从命令行向一个 topic 内发布数据:
ros2 topic pub ''
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}}"
其中 --once 是一个可选参数,其意味着 “发布一个消息然后退出”。
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}}"
这里 --rate 1 代替了 --once 选项,意思是以一个 1Hz 的稳定流发布命令。
使用该命令可以发布数据的速率:
ros2 topic hz /turtle1/pose
输出类似如下:
average rate: 59.354
min: 0.005s max: 0.027s std dev: 0.00284s window: 58
service 是另一种节点通信方法。service 基于 call-response 模式,与 publisher-subscriber 模式相对。topic 允许节点 suscribe 数据流并获得持续更新,而 service 仅在被 client 指定调用时才会提供数据。
service 的使用方法与 topic 类似。
为了查看参数的节点归属,可以输入如下命令:
ros2 param list
就可以看到每个节点的子命名空间,紧接着的是每个节点的参数:
/teleop_turtle:
scale_angular
scale_linear
use_sim_time
/turtlesim:
background_b
background_g
background_r
use_sim_time
需要注意的是,每个节点都有参数 use_sim_time,它并不是独一无二的。
该命令用于获取当前参数的值:
ros2 param get
例如探究上述 /turtlesim 内的参数 background_g:
ros2 param get /turtlesim background_g
其参数值为:
Integer value is: 86
现在就可以知道 background_g 保留一个整数值。
为了在运行时间内改变参数值,使用命令:
ros2 param set
例如:
ros2 param set /turtlesim background_r 150
可以将一个节点的所有参数值 “dump” 进一个文件以储存,使得之后能使用该命令:
ros2 param dump
例如,如果想储存节点 /turtlesim 当前的配置参数,可以输入命令:
ros2 param dump /turtlesim
终端将会返回消息:
Saving to: ./turtlesim.yaml
你将发现在该工作空间目录有一个新文件被写入了。打开该文件,会显示以下内容:
turtlesim:
ros__parameters:
background_b: 255
background_g: 86
background_r: 150
use_sim_time: false
该参数表需要手动加载。
为了使用在上一小节中保存的参数值,输入:
ros2 run --ros-args --params-file
与之前运行可执行文件方式一样,就是多了两个 flags: ‘–ros-args’ 和 ‘–params-file’,之后再接上想要加载的文件。
紧接着上一节的示例,想要读取该参数,需输入命令:
ros2 run turtlesim turtlesim_node --ros-args --params-file ./turtlesim.yaml
ROS 中常用的通信机制是话题(Topic)和服务(Service),但是在很多场景下,这两种通信机制往往满足不了所有需求。比如用话题发布运动目标,由于话题是单向通信,则需要另外订阅一个话题来获得机器人运动过程中的状态反馈。如果用服务发布运动目标,虽然可获得一次反馈信息,但是对于控制来说数据太少,而且当反馈迟迟没有收到时,就只能傻傻等待,做不了其他事情。
action 是一种类似于 Service 的问答通信机制,不同之处在于 action 带有连续反馈,可以不断反馈任务进度,也可以在任务过程中中止运行(actions are preemptable)。使用 action 发布机器人的运动目标,机器人在收到这个 action 后就开始运动,在运动过程中不断反馈当前的运动状态;过程中也可以随时取消运动,让机器人停止;当机器人完成运动目标后,action 返回任务完成的消息。
为了识别在 ROS 中所有的 action,运行此命令:
ros2 action list
记录发布到一个 topic 的消息可使用命令语句:
ros2 bag record
也可以同时记录多个话题:
ros2 bag record -o subset /turtle1/cmd_vel /turtle1/pose
选项 -o 允许给 bag 文件定义一个独一无二的名称。紧接着的字符串,subset 就是文件名。
该命令用来查看记录好的 bag:
ros2 bag info
该命令用于播放记录好的 bag:
ros2 bag play
注意,与 ROS 不同的是,该命令并不会显示播放的时长以及总时长。
在 ROS 2 内,创建一个新的功能包的命令语法为:
ros2 pkg create --build-type ament_cmake
例如使用语句:
ros2 pkg create --build-type ament_cmake --node-name my_node my_package
上述语句多使用了一个可选参数 --node-name,它产生了一个叫 my_node 的 Hello World 类型可执行文件(在src中创建了名为my_node.cpp的文件)。
在 build 的时候要是只想搭建功能包 my_package,可以运行:
colcon build --packages-select my_package
一般来说,使用以下命令会更方便:
ros2 pkg create --build-type ament_cmake <pkg-name> --dependencies [deps]
# ros2 pkg create --build-type ament_cmake cpp_parameters --dependencies rclcpp
--dependencies
后接所需要的依赖项,这样依赖项会被自动添加进 package.xml
和 CMakeLists.txt
内。
使用示例文件 publisher_member_function.cpp
#include // C++11日期和时间库
#include
#include
#include
#include "rclcpp/rclcpp.hpp" // 包含了 ROS2 系统的大多部分(rclcpp:ROS Client Library for C++)
#include "std_msgs/msg/string.hpp" // 这里跟 ROS1 区别不大
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */
// 通过从 rclcpp::Node 继承创建节点类。代码中的每一个this都指代这个node
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0) //节点被命名为了 minimal_publisher,初始化count_为0
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer( // 初始化timer_
500ms, std::bind(&MinimalPublisher::timer_callback, this)); // 一秒执行两次timer_callback
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());// 确保每一个被发布的消息都被打印到控制台
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv); // 初始化 ROS2
rclcpp::spin(std::make_shared<MinimalPublisher>()); // 开始从节点处理数据
rclcpp::shutdown();
return 0;
}
使用文本编译器打开 package.xml 文件,复制以下命令:
<depend>rclcpp</depend>
<depend>std_msgs</depend>
这声明了功能包在它的代码执行过程中需要 rclpp 和 std_msgs。
打开文件 CMakeLists.txt,添加依赖项:
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
之后,添加可执行文件并命名为 talker 这样就可以使用节点运行 ros2 run:
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
ament_target_dependencies
的用法为:
ament_target_dependencies( [dependencies])
该 CMake 宏用来创建可执行节点并且与依赖项关联起来。
最后,添加 install(TARGETS…) 部分,这样 ros2 run 可以找到该可执行文件:
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
为了 install 启动文件和节点,可以使用 install()
宏放置在文件最后,但在宏 ament_package()
之后, 示例如下:
# Install launch files
install(
DIRECTORY launch
DESTINATION share/${PROJECT_NAME}
)
# Install nodes
install(
TARGETS [node-names]
DESTINATION lib/${PROJECT_NAME}
)
在精简了 CMakeLists.txt 后,它的样子如下:
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
使用示例文件 subscriber_member_function.cpp
#include
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
在这里程序与 Publisher 程序基本相同,除了 spin 的含义。对于 publisher 节点,spin 意味着启动计时器,但对于 subscriber 来说,它只是意味着准备在消息到来时接收消息。
msg 和 srv 文件内构造与 ROS1 相同。
创建一个称作 Num.msg 的新文件,里面写入代码:
int64 num
该自定义消息,传输一个名为num的64位整数。
创建一个称作 AddThreeInts.srv 的新文件,里面写入代码:
int64 a
int64 b
int64 c
---
int64 sum
该自定义消息,它 request 三个名为 a、b 和 c 的整数,并用一个名为 sum 的整数 respond。
要将您定义的接口转换为特定于语言的代码(如c++和Python),以便在这些语言中使用它们,请向CMakeLists.txt添加以下代码行:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"srv/AddThreeInts.srv"
)
由于接口依赖 rosidl_default_generators 来生成语言指定代码,需要声明一个依赖性:
rosidl_default_generators
rosidl_default_runtime
rosidl_interface_packages
与之前的程序相比,微调了一些地方:
#include
#include
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // CHANGE
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = tutorial_interfaces::msg::Num(); // CHANGE
message.num = this->count_++; // CHANGE
RCLCPP_INFO(this->get_logger(), "Publishing: '%d'", message.num); // CHANGE
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // CHANGE
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
#include
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // CHANGE
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const tutorial_interfaces::msg::Num::SharedPtr msg) const // CHANGE
{
RCLCPP_INFO(this->get_logger(), "I heard: '%d'", msg->num); // CHANGE
}
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
tutorial_interfaces
使用 ROS2 启动命令运行一个单独的启动文件将会立即启动整个系统——所有节点及其配置。这在程序的运行中是非常方便的。需要注意的是,ROS2 与 ROS1 在 Launch 文件上有很大的不同。ROS1 使用的是 .launch
文件,ROS2 使用的是 .py
文件。
示例文件 turtlesim_mimic_launch.py(版本:Eloquent)
// 引入python的launch模块
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
node_namespace='turtlesim1',
node_executable='turtlesim_node',
node_name='sim'
),
Node(
package='turtlesim',
node_namespace='turtlesim2',
node_executable='turtlesim_node',
node_name='sim'
),
Node(
package='turtlesim',
node_executable='mimic',
node_name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
)
])
这里程序创建了三个节点,所有的都来自于 turtlesim 功能包。系统的目标是打开两个 turtlesim 窗口,其中一只乌龟模仿另一只乌龟的移动。
其中,如下语句为launch文件描述的开始:
def generate_launch_description():
return LaunchDescription([
])
下述的两个节点:
Node(
package='turtlesim',
node_namespace='turtlesim1',
node_executable='turtlesim_node', // 可执行程序的名称
node_name='sim'
),
Node(
package='turtlesim',
node_namespace='turtlesim2',
node_executable='turtlesim_node',
node_name='sim'
),
注意,这两个节点之间的唯一区别是它们的 namespace 值。独一无二的 namespace 允许系统在没有 node name 或 topic name 冲突的情况下启动两个模拟器。
在这个系统中,两只海龟都接收到关于同一主题的命令,并发布它们关于同一主题的姿态。如果没有独一无二的 namespace,就没有办法区分属于某个turtle的消息和属于另一个turtle的消息。
最后的一个节点也来自于 turtlesim 功能包,但是有不同的可执行文件:
Node(
package='turtlesim',
node_executable='mimic',
node_name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
)
mimic
的 /input/pose
topic 被重映射为了 /turtlesim1/turtle1/pose
;/output/cmd_vel
话题被重映射为了 /turtlesim2/turtle1/cmd_vel
。这意味着 mimic 会订阅 /turtlesim1/sim
的位姿 topic 并重新发布它给 /turtlesim2/sim
的速度命令 topic 来订阅。
这意味着mimic将订阅/turtlesim1/sim的pose主题,并为/turtlesim2/sim的velocity命令主题重新发布要订阅的主题。
Lauch 文件有两种运行方法:
ros2 launch <launch_file_name> # ros2 launch turtlesim_mimic_launch.py
ros2 launch <package_name> <launch_file_name>
默认情况下,它将创建以下目录作为src目录的对等目录:
build
:用来存储中间文件。对于每一个功能包,将创建一个子文件夹,如 CMake 被调用的子文件夹。install
:每个功能包被配置到的位置。在默认情况下,每一个功能包都会被配置到一个独立的子目录中。log
:包含关于每次 colcon 调用的各种日志信息。安装目录是每个包将被安装到的位置。默认情况下,每个包将安装到单独的子目录中。
日志目录包含关于每次colcon调用的各种日志信息。
需要注意的是,与 ROS1 使用到的 catkin 不同,colcon 没有 devel 目录。
在命令行上使用 ros2 pkg create
可以生成一个基本的 CMake 框架,然后将基本构建信息收集到两个文件中:package.xml
和 CMakeLists.txt
。
package.xml
必须包含所有依赖项和一些元数据,以便让 colcon 为这个功能包找到正确的构建顺序,在CI中安装所需的依赖项,并为bloom发行版提供信息。
CMakeLists.txt
包含构建+功能包的可执行文件和库 的命令,这将是本届主要的关注点。
一个 ament 功能包的 CMakeLists.txt
的基本框架包含:
cmake_minimum_required(VERSION 3.5)
project(my_project)
ament_package()
参数 project
表示功能包名称,必须要与在 package.xml
内的功能包名称相同。
项目设置由 ament_package()
完成,每个功能包它必须也只能被调用一次。ament_package()
配置 package.xml
,注册带 ament 索引的功能包,并为 CMake 搭建配置文件,以便其他使用 find_package
的功能包可以找到它。因为 ament_package()
从 CMakeLists.txt
收集了很多信息,它需要被 CMakeLists.txt
最后调用。尽管可以在调用 install
函数复制文件和目录之后调用 ament_package()
,但是将 ament_package()
恒作为最后的调用更简便。
ament_package()
可以被提供的额外参数:
CONFIG_EXTRAS
:CONFIG_EXTRAS_POST
:有两个主要的目标需要搭建:由 add_library
搭建的库文件以及由 add_executable
搭建的可执行文件。
由于C/ c++中头文件和实现文件的分离,所以并不总是需要将这两个文件作为参数添加到 add_library
/ add_executable
中。
以下是建议的最佳做法:
add_library
或 add_executable
调用中被显示引用。target_include_directories(my_target
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
这个命令在 build 期间内和被 installed 时添加了所有在文件夹 ${CMAKE_CURRENT_SOURCE_DIR}/include
内的文件到公共接口。
原则上,如果${CMAKE_CURRENT_SOURCE_DIR}
和${CMAKE_INSTALL_DIR}
同时调用了include和顶层文件夹,那么使用生成器表达式在这里是没有必要的,但它是非常常见的。
有两种方式来连接功能包和新的依赖项:
第一种为推荐的方式,使用 ament 宏 ament_target_dependencies
。举个栗子,假设想连接 my_target 和线性几何库 Eigen3。
find_package(Eigen3 REQUIRED)
ament_target_dependencies(my_target Eigen3)
这包含了必须的头文件和库文件,并且它们的依赖项会被项目正确找到。它也确保了所有依赖项的 include 目录顺序正确,在使用 overlay 工作空间的时候。
第二种方式是使用 target_link_libraries
。
find_package(Eigen3 REQUIRED)
target_link_libraries(my_target Eigen3::Eigen)
这也将包括必要的头文件、库和它们的依赖关系,但是与 ament_target_dependencies
相比,在使用 overlay 工作区时,它可能不会正确地对依赖关系排序。