ROS2 Node的api相比ROS1改动较大,提倡使用面向对象编程的思想提高代码可维护性。
Package build type in ROS2:
- ament_cmake: 和ros1一样的package的结构
- ament_python: ros2中针对纯python构建的package的结构
创建一个简单的cpp的package:
ros2 pkg create --build-type ament_cmake cpp_pkg --dependencies rclcpp std_msgs
写一个简单的publisher:
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class Talker : public rclcpp::Node
{
public:
Talker(): Node("talker_cpp_node"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("talker_topic", 10);
timer_ = this->create_wall_timer( 500ms, std::bind(&Talker::timer_callback, this ));
}
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);
rclcpp::spin(std::make_shared<Talker>());
rclcpp::shutdown();
return 0;
}
《ROS 2机器人开发从入门到实践》2.5.2用得到的C++新特性]
-
ROS2中,node及是一个共享指针,通过std::make_shared()模板函数创建。同时,node继承rclcpp::Node这个类。
-
#include <functional>
: functional是函数包装器的头文件。常见的函数有三种,直接定义的普通函数,类里面的成员函数,和lamda函数。包装器可以对所有函数类型进行封装,不同于python的函数decorator,c++的包装器像是在给函数起“别名”。上面的程序中的std::bind()
函数,是安全调用成员函数的方式。
ros2写一个简单的publisher时候,采用OOP继承一个类的方式比ros1采用nodehandle操作node的方式复杂得多。采用面向对象后,cpp的代码比python的也复杂的多,也许这就是cpp的魅力吧。
Let's say上面这个代码片段的名字是talker.cpp; 进一步的,我们需要对包的CMakeLists.txt进行操作:
修改后的CMakeLists:
cmake_minimum_required(VERSION 3.8)
project(cpp_pkg)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
##### Look Me ! #####
add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME}
)
##### Look Me ! #####
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# comment the line when a copyright and license is added to all source files
set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# comment the line when this package is in a git repo and when
# a copyright and license is added to all source files
set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
和ros1或者一般的cmake构建的cpp工程类似,需要先添加可执行文件;随后一般我们要target_link_libraries(),ros2采用了新的语法,ament_target_dependencies( dependencie1 dependencie2 ...)这样对单独的可执行文件声明依赖的操作,看起来更优雅。 最后,ros2采用了install机制来安装最后生成的可执行文件,这一点和ros1有区别。而install语法端只需要声明一次,把需要的可执行文件添加进去即可,如下:
install(TARGETS
exec1
exec2
DESTINATION lib/${PROJECT_NAME}
)
创建一个简单的python的package:
ros2 pkg create --build-type ament_python py_pkg --dependencies rclpy std_msgs
ament_python看起来就是一个纯python包的结构,使用setuptools。一般情况下,只需要在和package的一级同名文件夹下写代码就可以了,cd进入这个文件夹,可以看到有一个__init__.py的空文件,这个文件是告诉python解释器这个文件夹是一个可以import的python的package。
在py_pkg/py_pkg中新建一个文件,let's call it talker.py, 写一个简单的publisher:
import rclpy
from rclpy.node import Node
import sys
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__("Talker")
self.publisher = self.create_publisher(String, "talker_topic", 10)
self.count = 0
self.timer_ = self.create_timer(0.5, self.timer_callback)
def timer_callback(self):
message = String()
message.data = "Hello ROS2," + str(self.count)
self.get_logger().info("Publishing:" + message.data)
self.publisher.publish(message)
self.count += 1
def main():
rclpy.init()
node = Talker()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == '__main__':
main()
可以看到,同样的在python中创建ROS2的Node也采用继承的方式,这可能和rcl底层构建有关系,可能在写rcl的时候就这么规划了。
相比于CPP的代码,用python实现一个node明显代码简单了很多。但一样的是,都需要继承rclcpp/rclpy提供的Node类。
随后,不同于ros1,我们在ros2中有了独立的python构建类型,需要多一步设置: 在setup函数中的entry_points变量中的'console_scripts'的list中添加我们需要的脚本对应的main函数,有点像CMakeLists中add_executable()
setup(
entry_points={
'console_scripts': [
"talker_py = py_pkg.talker:main"
],
},
)
其中,talker_py作为这个脚本的"可执行文件名"。(为什么有双引号,因为python不需要编译啊~)
笔者还是喜欢ros1中的xml格式,拓展性和可读性其实不错,且即改即用,很方便。
ROS2的launch文件确实抽象了点,ROS2的launch文件提倡用python写,launch文件的源代码也是python写的,因此拓展性也更好,支持了xml和yaml写launch文件。一个package中,需要编写launch文件的话且像ros1一样让package识别launch文件,还需要在 CMakeLists / setup.py 做launch文件的声明,挺麻烦。。。。。。
- 对于
ament_cmake
,加入:
# Install launch files.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)
- 对于
ament_python
,加入:
import os
from glob import glob
# Other imports ...
package_name = 'py_launch_example'
setup(
# Other parameters ...
data_files=[
# ... Other data files
# Include all launch files.
(os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[py]or[yaml]or[xml]'))) # ADD THIS LINE ! 用python的launch就用.py的后缀,yaml和xml同理
]
)
详细的ros2中launch的使用请参考链接
按照官方文档,推荐在package.xml中声明依赖ros2launch,不加其实也没关系,加一下可以治强迫症。
<exec_depend>ros2launch</exec_depend>
笔者觉得在编写launch体验上ROS2比ROS1在效率和可读性上差的不止一点半点。即便如此,笔者更喜欢沿用xml的格式写ros2的launch文件。
Created on September 1, 2024.