创建包

# ament_cmake,默认
ros2 pkg create --build-type ament_cmake <package_name>
# cmake
ros2 pkg create --build-type cmake <package_name>

迁移包

<depend>foo</depend>
<!-- <depend>等于以下 -->
<build_depend>foo</build_depend>
<build_export_depend>foo</build_export_depend>
<exec_depend>foo</exec_depend>
<!-- ros2没有以下 -->
<run_depend>foo</run_depend>

迁移接口

消息,服务和动作在ROS统称接口(interfaces)

package依赖添加

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<depend>message_package</depend>

CMake添加C++17支持

set(CMAKE_CXX_STANDARD 17)
find_package(rosidl_default_generators REQUIRED)
find_package(message_package REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "msg/Sphere.msg"
  "srv/AddThreeInts.srv"
  DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)

自定义消息文档: https://docs.ros.org/en/rolling/Tutorials/Beginner-Client-Libraries/Custom-ROS2-Interfaces.html

迁移C++包

构建工具

使用colcon,教程: https://docs.ros.org/en/rolling/Tutorials/Beginner-Client-Libraries/Colcon-Tutorial.html

构建系统

ROS2 使用ament构建

package.xml里面加上

<buildtool_depend>ament_cmake</buildtool_depend>
<!-- your dependencies -->
<export>
  <build_type>ament_cmake</build_type>
</export>

找包方式

find_package(ament_cmake REQUIRED)
find_package(component1 REQUIRED)
# ...
find_package(componentN REQUIRED)

在所有包注册好后调用ament_package

catkin_package里面的CATKIN_DEPENDSINCLUDE_DIRSLIBRARIES改为使用三条cmake函数ament_export_dependenciesament_export_include_directoriesament_export_libraries,这几条命令需要在ament_package之前调用

替换add_message_files, add_service_filesgenerate_messagesrosidl_generate_interfaces

rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
  DEPENDENCIES std_msgs
)

单元测试

检查器(Linters)

更新源码

消息,服务和动作

ROS2的消息,服务和动作都在不同的命名空间下,例如#include <my_interfaces/msg/my_message.hpp>my_interfaces::msg::MyMessage

共享指针的类型也变了:my_interfaces::msg::MyMessage::SharedPtr

更详细的文档: generated C++ interfaces

使用服务

ROS2服务没有返回值了

// ROS 1 style is in comments, ROS 2 follows, uncommented.
// #include "nav_msgs/GetMap.h"
#include "nav_msgs/srv/get_map.hpp"

// bool service_callback(
//   nav_msgs::GetMap::Request & request,
//   nav_msgs::GetMap::Response & response)
void service_callback(
  const std::shared_ptr<nav_msgs::srv::GetMap::Request> request,
  std::shared_ptr<nav_msgs::srv::GetMap::Response> response)
{
  // ...
  // return true;  // or false for failure
}

ros::Time迁移

  • 把所有的ros::Time改为rclcpp::Time
  • 如果代码中有用到std_msgs::Time:
    • 把所有td_msgs::Time实例改为builtin_interfaces::msg::Time
    • 把所有包含#include "std_msgs/time.h改为#include "builtin_interfaces/msg/time.hpp"
    • 把所有std_msgs::Time中的nsec改为builtin_interfaces::msg::Timenanosec
  • ros::Time::now()改为node->now()
namespace rclcpp {
  ...
  explicit Clock(rcl_clock_type_t clock_type = RCL_SYSTEM_TIME);
}

[!NOTE] ROS2中不同时钟类型(clock_type)的时间(rclcpp::Time)类型是不能进行运算比较等操作的。rclcpp::Clock().now()返回的是系统时间, 加参数RCL_ROS_TIME后等同ros::Time::now(),默认是系统时间,也就是WallTime

ROS2中rclcpp::Time如果没初始化就做比较会崩溃,报错,和上面的问题是一样的,不像ROS1,默认用0给ros::Time赋值

terminate called after throwing an instance of 'std::runtime_error'
  what():  can't subtract times with different time sources [2 != 1]

用以下代码检查报文是否初始化时间戳

auto locateTime = rclcpp::Time(locate.header.stamp);
if (locateTime.get_clock_type() == RCL_CLOCK_UNINITIALIZED) {
    LOGW_HZ(0.5, "定位时间戳类型错误");
}

ros::Rate迁移

ros::Rate改为rclcpp::Rate

Boost

不受影响,注意版本接口就行,但是ROS2自带的函数和智能指针全用std标准库的了

线程和锁

  • boost::mutex::scoped_lock -> std::unique_lock<std::mutex>
  • boost::mutex -> std::mutex
  • #include <boost/thread/mutex.hpp> -> #include <mutex>

无序map

boost改为标准c++的

function

boost改为标准c++的

实际迁移例子

https://docs.ros.org/en/rolling/How-To-Guides/Migrating-from-ROS1/Migrating-CPP-Packages.html#example-converting-an-existing-ros-1-package-to-ros-2

迁移Python包

迁移launch文件

设计文档: https://docs.ros.org/en/rolling/Tutorials/Intermediate/Launch/Launch-system.html

例子: http://design.ros2.org/articles/roslaunch_xml.html

迁移标签

launch

ROS2中根元素为launch

node

  • type -> exec
  • ns -> namespace
  • 移除machine, respawn_delay, clear_params.

param

  • ROS2没有全局参数,不支持的标签有type, textfile, binfile, executable
  • command属性变为value="$(command '...' )"

类型规则

例子

<node pkg="my_package" exec="my_executable" name="my_node">
   <!--A string parameter with value "1"-->
   <param name="a_string" value="'1'"/>
   <!--A integer parameter with value 1-->
   <param name="an_int" value="1"/>
   <!--A float parameter with value 1.0-->
   <param name="a_float" value="1.0"/>
   <!--A string parameter with value "asd"-->
   <param name="another_string" value="asd"/>
   <!--Another string parameter, with value "asd"-->
   <param name="string_with_same_value_as_above" value="'asd'"/>
   <!--Another string parameter, with value "'asd'"-->
   <param name="quoted_string" value="\'asd\'"/>
   <!--A list of strings, with value ["asd", "bsd", "csd"]-->
   <param name="list_of_strings" value="asd, bsd, csd" value-sep=", "/>
   <!--A list of ints, with value [1, 2, 3]-->
   <param name="list_of_ints" value="1,2,3" value-sep=","/>
   <!--Another list of strings, with value ["1", "2", "3"]-->
   <param name="another_list_of_strings" value="'1';'2';'3'" value-sep=";"/>
   <!--A list of strings using an strange separator, with value ["1", "2", "3"]-->
   <param name="strange_separator" value="'1'//'2'//'3'" value-sep="//"/>
</node>

参数组

<node pkg="my_package" exec="my_executable" name="my_node" ns="/an_absoulute_ns">
   <param name="group1">
      <param name="group2">
         <param name="my_param" value="1"/>
      </param>
      <param name="another_param" value="2"/>
   </param>
</node>

rosparam

从其他文件中加载配置,改为from

<node pkg="my_package" exec="my_executable" name="my_node" ns="/an_absoulute_ns">
   <param from="/path/to/file"/>
</node>

remap

只准在node中使用

include

https://docs.ros.org/en/rolling/How-To-Guides/Migrating-from-ROS1/Migrating-Launch-Files.html#replacing-an-include-tag

arg

  • value标签不允许,使用let标签
  • doc变为description
  • include标签里面时,ifunlessdescription都不允许

tf到tf2静态变换迁移

<node pkg="tf2" exec="static_transform_publisher" name="vehicleTransPublisher"
          args="-$(arg sensorOffsetX) -$(arg sensorOffsetY) 0 0 0 0 /sensor /vehicle 1000"/>

<node pkg="tf2_ros" exec="static_transform_publisher" name="vehicleTransPublisher" args="-$(var sensorOffsetX) -$(var sensorOffsetY) 0 0 0 0 /sensor /vehicle"/>

迁移参数

迁移脚本

自动转换工具

Magical ROS 2 Conversion Tool

启动文件迁移器,用于将ROS 1的XML启动文件转换为ROS 2的Python启动文件:https://github.com/aws-robotics/ros2-launch-file-migrator

亚马逊已经提供了他们用于从ROS 1迁移到ROS 2的工具,链接如下:https://github.com/awslabs/ros2-migration-tools/tree/master/porting_tools

rospy2 Python项目,用于自动将rospy调用转换为rclpy调用

其他迁移技巧

ROS命令行

  • rosrun -> ros2 run
  • rosparam -> ros2 param
  • 其他类似

ROS命令行参数

ROS2命令行参数必须在--ros-args之后

ros2 run some_package some_ros_executable --ros-args -r foo:=bar

参数

ros2 run some_package some_ros_executable --ros-args -p my_param:=value

更改节点名

ros2 run some_package some_ros_executable --ros-args -r __node:=new_node_name

更改命名空间

ros2 run some_package some_ros_executable --ros-args -r __ns:=/new/namespace

ROS发布订阅

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>

class MyNode : public rclcpp::Node
{
public:
  MyNode() : Node("my_node")
    {
      publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
      subscription_ = this->create_subscription<sensor_msgs::msg::PointCloud2>(
      "topic", rclcpp::SensorDataQoS(), [this] (sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) { topic_callback(msg); });
    }
private:
  void topic_callback(sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
    }
private:
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
}

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MyNode>());
  rclcpp::shutdown();
  return 0;
}

使用lambda表达式可以避免使用std::bind绑定订阅回调函数时出现以下错误(不一定出现):

  113 |     const_shared_ptr_callback_ = callback;
      |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
In file included from /usr/include/c++/9/future:48,
                 from /opt/ros/foxy/include/rclcpp/executors.hpp:18,
                 from /opt/ros/foxy/include/rclcpp/rclcpp.hpp:146,
                 from /home/jiang/code/general_robot/src/locate_3d/src/LidarLocate.hpp:5,
                 from /home/jiang/code/general_robot/src/locate_3d/src/LidarLocate.cpp:1:
/usr/include/c++/9/bits/std_function.h:462:7: note: candidate: ‘std::function<_Res(_ArgTypes ...)>& std::function<_Res(_ArgTypes ...)>::operator=(const std::function<_Res(_ArgTypes ...)>&) [with _Res = void; _ArgTypes = {std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >}]’
  462 |       operator=(const function& __x)
      |       ^~~~~~~~
/usr/include/c++/9/bits/std_function.h:462:33: note:   no known conversion for argument 1 from ‘std::_Bind<void (LidarLocate::*(LidarLocate*, boost::arg<1>))(std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >)>’ to ‘const std::function<void(std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >)>&’
  462 |       operator=(const function& __x)
      |                 ~~~~~~~~~~~~~~~~^~~
/usr/include/c++/9/bits/std_function.h:480:7: note: candidate: ‘std::function<_Res(_ArgTypes ...)>& std::function<_Res(_ArgTypes ...)>::operator=(std::function<_Res(_ArgTypes ...)>&&) [with _Res = void; _ArgTypes = {std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >}]’
  480 |       operator=(function&& __x) noexcept
      |       ^~~~~~~~
/usr/include/c++/9/bits/std_function.h:480:28: note:   no known conversion for argument 1 from ‘std::_Bind<void (LidarLocate::*(LidarLocate*, boost::arg<1>))(std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >)>’ to ‘std::function<void(std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >)>&&’
  480 |       operator=(function&& __x) noexcept
      |                 ~~~~~~~~~~~^~~
/usr/include/c++/9/bits/std_function.h:494:7: note: candidate: ‘std::function<_Res(_ArgTypes ...)>& std::function<_Res(_ArgTypes ...)>::operator=(std::nullptr_t) [with _Res = void; _ArgTypes = {std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >}; std::nullptr_t = std::nullptr_t]’
  494 |       operator=(nullptr_t) noexcept
      |       ^~~~~~~~
/usr/include/c++/9/bits/std_function.h:494:17: note:   no known conversion for argument 1 from ‘std::_Bind<void (LidarLocate::*(LidarLocate*, boost::arg<1>))(std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >)>’ to ‘std::nullptr_t’
  494 |       operator=(nullptr_t) noexcept
      |                 ^~~~~~~~~
/usr/include/c++/9/bits/std_function.h:523:2: note: candidate: ‘template<class _Functor> std::function<_Res(_ArgTypes ...)>::_Requires<std::function<_Res(_ArgTypes ...)>::_Callable<typename std::decay<_Functor>::type>, std::function<_Res(_ArgTypes ...)>&> std::function<_Res(_ArgTypes ...)>::operator=(_Functor&&) [with _Functor = _Functor; _Res = void; _ArgTypes = {std::shared_ptr<const geometry_msgs::msg::PoseStamped_<std::allocator<void> > >}]’
  523 |  operator=(_Functor&& __f)
      |  ^~~~~~~~

代码的C++标准为17

另外

rclcpp::Subscriptiontake函数:

rclcpp::Subscriptiontake函数用于从订阅的主题中取出一条消息。这个函数会尝试从订阅的主题中取出一条消息,并将其放入提供的消息实例中。如果成功取出一条消息,函数会返回true,否则返回false

这是take函数的原型:

bool take(MessageT & message_out, rmw_message_info_t & message_info_out);

其中,MessageT是消息的类型,message_out是一个消息实例的引用,用于存储取出的消息。message_info_out是一个rmw_message_info_t实例的引用,用于存储关于取出的消息的元信息,如发布者的信息。==不确定能不能自己用==

动作的设计文档:http://design.ros2.org/articles/actions.html

动作客户端例子

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <your_action_pkg/action/YourAction.hpp>  // 替换为你的动作消息头文件

class ActionClientNode : public rclcpp::Node
{
public:
  ActionClientNode() : Node("action_client_node")
  {
    this->client_ptr_ = rclcpp_action::create_client<your_action_pkg::action::YourAction>(this,
      "your_action_name");  // 替换为你的动作名称

    // 创建一个目标
    auto goal_msg = your_action_pkg::action::YourAction::Goal();
    // ... 填充目标消息

    // 发送目标,并绑定结果回调函数
    this->client_ptr_->async_send_goal(goal_msg, std::bind(&ActionClientNode::goal_response_callback, this, std::placeholders::_1));
  }

private:
  void goal_response_callback( rclcpp_action::ClientGoalHandle<your_action_pkg::action::YourAction>::SharedPtr goal_handle)
  {
    if (!goal_handle) {
      RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server");
    } else {
      RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result");
      goal_handle->async_result(std::bind(&ActionClientNode::result_callback, this, std::placeholders::_1));
    }
  }

  void feedback_callback( rclcpp_action::ClientGoalHandle<your_action_pkg::action::YourAction>::SharedPtr goal_handle,
    const std::shared_ptr<const your_action_pkg::action::YourAction::Feedback> feedback)
  {
    
  }

  void result_callback(const rclcpp_action::ClientGoalHandle<your_action_pkg::action::YourAction>::WrappedResult & result)
  {
    switch(result.code) {
      case rclcpp_action::ResultCode::SUCCEEDED:
        // 处理成功的结果
        break;
      case rclcpp_action::ResultCode::ABORTED:
        // 处理被服务器中止的结果
        break;
      case rclcpp_action::ResultCode::CANCELED:
        // 处理被客户端取消的结果
        break;
      default:
        // 处理其他结果
        break;
    }
  }

  rclcpp_action::Client<your_action_pkg::action::YourAction>::SharedPtr client_ptr_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ActionClientNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

动作服务端例子

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <your_action_pkg/action/YourAction.hpp>  // 替换为你的动作消息头文件

class ActionServerNode : public rclcpp::Node
{
public:
  ActionServerNode() : Node("action_server_node")
  {
    using GoalHandle = rclcpp_action::ServerGoalHandle<your_action_pkg::action::YourAction>;

    this->server_ptr_ = rclcpp_action::create_server<your_action_pkg::action::YourAction>(this,
      "your_action_name",  // 替换为你的动作名称
      std::bind(&ActionServerNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
      std::bind(&ActionServerNode::handle_cancel, this, std::placeholders::_1),
      std::bind(&ActionServerNode::handle_accepted, this, std::placeholders::_1));
  }

private:
  rclcpp_action::GoalResponse handle_goal(
    const rclcpp_action::GoalUUID & uuid,
    your_action_pkg::action::YourAction::Goal::ConstSharedPtr goal)
  {
    // 处理新的目标请求
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  rclcpp_action::CancelResponse handle_cancel(
    std::shared_ptr<rclcpp_action::ServerGoalHandle<your_action_pkg::action::YourAction>> goal_handle)
  {
    // 处理取消请求
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  void handle_accepted( std::shared_ptr<rclcpp_action::ServerGoalHandle<your_action_pkg::action::YourAction>> goal_handle)
  {
    // 处理已接受的目标
  }

  rclcpp_action::Server<your_action_pkg::action::YourAction>::SharedPtr server_ptr_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ActionServerNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

服务客户端例子

#include <rclcpp/rclcpp.hpp>
#include <your_srv_pkg/srv/YourService.hpp>  // 替换为你的服务消息头文件

class ServiceClientNode : public rclcpp::Node
{
public:
  ServiceClientNode() : Node("service_client_node")
  {
    this->client_ptr_ = this->create_client<your_srv_pkg::srv::YourService>("your_service_name");  // 替换为你的服务名称

    // 创建一个服务请求
    auto request = std::make_shared<your_srv_pkg::srv::YourService::Request>();
    // ... 填充请求消息

    // 发送服务请求,并绑定响应回调函数
    this->client_ptr_->async_send_request(request, std::bind(&ServiceClientNode::response_callback, this, std::placeholders::_1));
  }

private:
  void response_callback(const typename your_srv_pkg::srv::YourService::Response::SharedPtr response)
  {
    // 处理服务响应
  }

  rclcpp::Client<your_srv_pkg::srv::YourService>::SharedPtr client_ptr_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ServiceClientNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

服务服务端例子

#include <rclcpp/rclcpp.hpp>
#include <your_srv_pkg/srv/YourService.hpp>  // 替换为你的服务消息头文件

class ServiceServerNode : public rclcpp::Node
{
public:
  ServiceServerNode() : Node("service_server_node")
  {
    this->server_ptr_ = this->create_service<your_srv_pkg::srv::YourService>(
      "your_service_name",  // 替换为你的服务名称
      std::bind(&ServiceServerNode::handle_service, this, std::placeholders::_1, std::placeholders::_2));
  }

private:
  void handle_service(
    const your_srv_pkg::srv::YourService::Request::SharedPtr request,
    your_srv_pkg::srv::YourService::Response::SharedPtr response)
  {
    // 处理服务请求,并填充响应消息
  }

  rclcpp::Service<your_srv_pkg::srv::YourService>::SharedPtr server_ptr_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ServiceServerNode>();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

函数替换:

  • getNumSubscribers() -> get_subscription_count()

跳过包编译

ROS1中在包根目录touch CATKIN_IGNORE,ROS2则是touch COLCON_IGNORE

消息修改

命名规则

  • 消息字段为xxxx_xxxx命名风格
  • 消息常量为XXXX_XXXX命名风格

常用消息迁移

ROS1类型 ROS2类型 匹配正则表达式
Header std_msgs/Header ^\s*Header
time builtin_interfaces/Time ^\s*time

单独编译包

单独编译某个包并把cmake输出打印到命令行

colcon build --event-handlers console_direct+ --packages-select foo_package

CMake打印所有当前环境变量

get_cmake_property(_variableNames VARIABLES)
foreach (_variableName ${_variableNames})
    message(STATUS "${_variableName}=${${_variableName}}")
endforeach()

PCL

ROS2中没有pcl_ros包了,需要

find_package(pcl_conversions REQUIRED)
find_package(PCL REQUIRED COMPONENTS common io)
link_directories(${PCL_LIBRARY_DIRS})
add_definitions(${PCL_DEFINITIONS})

老的tf的一些函数实现

#include <geometry_msgs/msg/quaternion.hpp>
#include <tf2_geometry_msgs/tf2_geometry_msgs.h>
#include <tf2/LinearMath/Quaternion.h>

namespace tf
{

inline geometry_msgs::msg::Quaternion createQuaternionMsgFromYaw(double yaw)
{
    tf2::Quaternion q;
    q.setRPY(0, 0, yaw);
    return tf2::toMsg(q);
}

inline geometry_msgs::msg::Quaternion createQuaternionMsgFromRollPitchYaw(double roll, double pitch, double yaw)
{
    tf2::Quaternion q;
    q.setRPY(roll, pitch, yaw);
    return tf2::toMsg(q);
}

inline tf2::Quaternion createQuaternionFromYaw(double yaw)
{
    tf2::Quaternion q;
    q.setRPY(0, 0, yaw);
    return q;
}

inline tf2::Quaternion createQuaternionFromRPY(double roll, double pitch, double yaw)
{
    tf2::Quaternion q;
    q.setRPY(roll, pitch, yaw);
    return q;
}
}

tf2没有tf::Pose,改用tf2::Transform

<tf2_geometry_msgs/tf2_geometry_msgs.h>中包含消息和tf2数据转换

tf2::fromMsg()tf2::toMsg()

  • geometry_msgs::msg::Pose<=>tf2::Transform
  • geometry_msgs::msg::Quaternion<=>tf2::Quaternion
  • geometry_msgs::msg::TransformStamped<=>tf2::Stamped<tf2::Transform>

另外还有把消息里面的timestamp转tf2的tf2::TimePointtf2_ros::fromMsg

tf2::getYawtf2::getEulerYPR<tf2/utils.h>里面

spdlog冲突

如果使用自己编译封装的spdlog库打印日志,rclcpp::init需要添加参数才不会冲突

rclcpp::InitOptions options;
options.auto_initialize_logging(false);
rclcpp::init(argc, argv, options);

ROS2CLion

https://www.jetbrains.com/help/clion/ros2-tutorial.html

ROS2适配CLion的顶层CMakeLists.txt

用这种方法不需要参考CLion官方的导入compile_commands.json的办法

把该CMakeLists.txt放到工作目录下,和src平级

cmake_minimum_required(VERSION 3.14)
project("ROS2 Master")

# usually I put this in a separate file include("/opt/ros/_common/Colcon.cmake")
function(colcon_add_subdirectories)
  cmake_parse_arguments(PARSE_ARGV 0 "ARG" "" "BUILD_BASE;BASE_PATHS" "")

  message("search criteria: ${ARGV}")

  execute_process(COMMAND colcon list
    --paths-only
    --base-paths ${ARG_BASE_PATHS}
    --topological-order
    ${ARG_UNPARSED_ARGUMENTS}
    OUTPUT_VARIABLE paths)
    string(STRIP "${paths}" paths)
    string(REPLACE "\n" ";" paths "${paths}"
  )

  MESSAGE("colcon shows paths ${paths}")

  foreach(path IN LISTS paths)
    message("...examining ${path}")
    # if(EXISTS "${path}/CMakeLists.txt")
    execute_process(COMMAND colcon info --paths "${path}" OUTPUT_VARIABLE package_info)
    if(NOT "${package_info}" MATCHES "type:[ \t]+(cmake|ros.ament_cmake|ros.cmake)")
      message("skipping non-cmake project")
    elseif(NOT "${package_info}" MATCHES "name:[ \t]+([^ \r\n\t]*)")
      message(WARNING "could not identify package at ${path}")
    else()
      set(name "${CMAKE_MATCH_1}")
      message("...adding package ${name} from path ${path}")
      MESSAGE("package info: ${package_info}")

      get_filename_component(BUILD_PATH "${name}" ABSOLUTE BASE_DIR "${ARG_BUILD_BASE}")

      add_subdirectory("${path}" "${BUILD_PATH}")
    endif()
  endforeach()
endfunction()

colcon_add_subdirectories(
 BUILD_BASE "${PROJECT_SOURCE_DIR}/build"
 BASE_PATHS "${PROJECT_SOURCE_DIR}/src/"
  # 指定单独编译包
# --packages-select locate_node
)

先编译一遍工程文件,然后source install/setup.bash,然后用命令行启动CLion打开工程选该CMakeLists.txt即可

使用该脚本会碰到自己写的包相互依赖出问题,因为脚本中使用的是add_subdirectory("${path}" "${BUILD_PATH}"),假设两个自己写的包package_a和package_b,如果pacakge_b依赖package_a,如果CMakeLists中出现以下语句会出问题:

find_package(package_a REQUIRED)
# 上面语句可以改为
find_package(package_a QUIET)

ament_target_dependencies(${PROJECT_NAME} package_a)
上面语句需要在顶层CMake中定义一个变量 USE_AMENT 开关ament,把因为QUIET导入包出问题的ament语句放进去

if (USE_AMENT)
  ament_target_dependencies(${PROJECT_NAME} package_a)
endif ()

不建议用该配置,修改后还是会出各种问题,因为不能用ament_target_dependencies了,和用纯CMake写工程差不多

ROS2 colcon跟ROS1的构建工具catkin_make区别在于前者是每个包单独CMake编译,后者是所有包一起CMake编译,catkin build没试过

原脚本地址: https://gist.github.com/rotu/1eac858b808b82bbf1b475f515e91636

tf到tf2

tf::Pose没了,需要用tf2::transform替代

geometry_msgs/PoseStampedtf2::transform的相互转换

// 创建一个PoseStamped消息
geometry_msgs::msg::PoseStamped pose_stamped_msg;

// 转换为tf2::Transform
tf2::Transform tf2_transform;
tf2::fromMsg(pose_stamped_msg.pose, tf2_transform);

// 创建一个tf2::Transform
tf2::Transform tf2_transform_new;

// 转换为PoseStamped消息
geometry_msgs::msg::PoseStamped pose_stamped_msg_new;
pose_stamped_msg_new.pose = tf2::toMsg(tf2_transform_new);

包里面链接同一包的消息

if(${rosidl_cmake_VERSION} VERSION_LESS 2.5.0)
    rosidl_target_interfaces(trajectory_analyzer_node
    planning_debug_tools "rosidl_typesupport_cpp")
else()
    rosidl_get_typesupport_target(
            cpp_typesupport_target planning_debug_tools "rosidl_typesupport_cpp")
    target_link_libraries(trajectory_analyzer_node "${cpp_typesupport_target}")
endif()

这里的trajectory_analyzer_node是节点名,planning_debug_tools是工程名

源码参考: https://github.com/autowarefoundation/autoware.universe/blob/bdd8d65d084b4505a092d97d0b05225e7303eef8/planning/planning_debug_tools/CMakeLists.txt#L29

CMake IN_LIST报错

错误示例:

CMake Error at /opt/ros/humble/share/ament_cmake_core/cmake/core/ament_execute_extensions.cmake:41 (if):
  if given arguments:

    "ament_cmake_core" "IN_LIST" "_ARG_EXCLUDE"

  Unknown arguments specified
Call Stack (most recent call first):
  /opt/ros/humble/share/ament_cmake_core/cmake/core/ament_package.cmake:66 (ament_execute_extensions)
  CMakeLists.txt:60 (ament_package)

CMakeLists.txtproject(xxx)后面加上如下

if(POLICY CMP0057)
    cmake_policy(SET CMP0057 NEW)
endif()

c++ param

[!NOTE] ROS2的param需要先声明后获取,而且launch文件中如果是double类型的值一定要用5.0而非5这种整型形式

例如

<launch>
  <node pkg="test_param" exec="test_param">
    <param name="a" value="0.0" />
  </node>
</launch>
int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    rclcpp::Node node("test_param");
    double a = 1.0;
    // node.declare_parameter("a", rclcpp::ParameterType::PARAMETER_DOUBLE);
    // 这种更方便
    node.declare_parameter("a", a);
    node.get_parameter("a", a);
    RCLCPP_INFO(node.get_logger(), "a: %lf", a);
}
node.declare_parameter("value_name", default_value);
bool result = node.get_parameter("value_name", value);

如果不先调用declare_parameter则后面的get_parameter取不到数据

[!NOTE] 一个参数声明一次就够了,多个地方使用的时候不能重复调用declare_parameter,不然程序会异常退出

ROS bag

ros1的bag包转ros2的bag包

https://gitlab.com/ternaris/rosbags

文档: https://ternaris.gitlab.io/rosbags/

pip install rosbags
# 然后把`~/.local/bin`加到PATH
rosbags-convert foo.bag
rosbags-convert foo.bag --dst /dir/path/to/save
# 然后在foo目录下能找到.db3的bag包还有metadata.yaml文件

默认存储为db3的sqlite格式,可以用zstd进行进一步压缩

# 例如
zstd 2023-11-15-14-31-41.db3
# output:
# 2023-11-15-14-31-41.db3 : 54.75%   (3678203904 => 2013660420 bytes, 2023-11-15-14-31-41.db3.zst)

压缩后剩下55%左右,如果指定高等级可以压缩更多

# 指定level(1-19)等级为10
zstd -10 2023-11-15-14-31-41.db3

压缩后剩下45%左右

[!TIP] ros2 bag record参数里面也可设置zstd压缩

命令行别名

# build package
alias colcon-bp='colcon build --symlink-install --event-handlers console_direct+ --cmake-args -Wno-dev -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja --packages-select'
# build all
alias colcon-ba='colcon build --symlink-install --event-handlers console_direct+ --cmake-args -Wno-dev -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja'

把该两条命令放在.bashrc.zshrc里面即可使这两条用命令编译包

正则表达式

使用Python脚本替换一些常用的类型和文件

#!/usr/bin/python3

# 该脚本用来初步替换代码中的内容为ROS2的写法

import re
import sys

# 创建一个列表,其中包含所有查找和替换的正则表达式对
cpp_regex = [
    (r'\b(\w*_msgs)\:\:(?!msg)(\w{1,})\b', r'\1::msg::\2'),
    (r'\bconst (\w*_msgs\:\:msg\:\:\w*?)(::)?ConstPtr\s*&\s*(\w*)', r'\1::ConstSharedPtr \3'),
    (r'(\w*\-\>header\.stamp)\.toSec\(\)', r'rclcpp::Time(\1).seconds()'),
    (r'ros\:\:Time\(\)\.fromSec\((laserCloudTime)\)', r'rclcpp::Time(\1).seconds()'),
    (r'ros\:\:Time ', r'rclcpp::Time '),
    (r'ROS_(\w+)\(((?:.|\n)*?)\);', r'RCLCPP_\1(node->get_logger(), \2);'),
    ('<time.h>', '<ctime>'),
    ('<math.h>', '<cmath>'),
    ('<stdio.h>', '<cstdio>'),
    ('<stdlib.h>', '<cstdlib>'),
    ('<ros/ros.h>', '<rclcpp/rclcpp.hpp>'),
    ('<std_msgs/Bool.h>', '<std_msgs/msg/bool.hpp>'),
    ('<std_msgs/Int32.h>', '<std_msgs/msg/int32.hpp>'),
    ('<std_msgs/UInt32.h>', '<std_msgs/msg/uint32.hpp>'),
    ('<std_msgs/String.h>', '<std_msgs/msg/string.hpp>'),
    ('<std_msgs/Int32MultiArray.h>', '<std_msgs/msg/int32_multi_array.hpp>'),
    ('<std_msgs/Float32MultiArray.h>', '<std_msgs/msg/float32_multi_array.hpp>'),
    ('<std_msgs/ColorRGBA.h>', '<std_msgs/msg/color_rgba.hpp>'),
    ('<nav_msgs/Path.h>', '<nav_msgs/msg/path.hpp>'),
    ('<nav_msgs/Odometry.h>', '<nav_msgs/msg/odometry.hpp>'),
    ('<geometry_msgs/Pose.h>', '<geometry_msgs/msg/pose.hpp>'),
    ('<geometry_msgs/Point.h>', '<geometry_msgs/msg/point.hpp>'),
    ('<geometry_msgs/Polygon.h>', '<geometry_msgs/msg/polygon.hpp>'),
    ('<geometry_msgs/PoseStamped.h>', '<geometry_msgs/msg/pose_stamped.hpp>'),
    ('<geometry_msgs/PointStamped.h>', '<geometry_msgs/msg/point_stamped.hpp>'),
    ('<geometry_msgs/PolygonStamped.h>', '<geometry_msgs/msg/polygon_stamped.hpp>'),
    ('<sensor_msgs/PointCloud2.h>', '<sensor_msgs/msg/point_cloud2.hpp>'),
    ('<sensor_msgs/Joy.h>', '<sensor_msgs/msg/joy.hpp>'),
    ('<visualization_msgs/Marker.h>', '<visualization_msgs/msg/marker.hpp>'),
    ('<tf/transform_datatypes.h>', '<tf2/transform_datatypes.h>'),
    ('<tf/transform_broadcaster.h>', '<tf2_ros/transform_broadcaster.h>'),
    ('tf::TransformBroadcaster', 'tf2_ros::TransformBroadcaster'),
    ('tf::Matrix3x3', 'tf2::Matrix3x3'),
    ('ros::Publisher ', 'rclcpp::Publisher<>::SharedPtr '),
    ('ros::Subscriber ', 'rclcpp::Subscription<>::SharedPtr '),
    ('tf::Vector3', 'tf2::Vector3'),
    ('tf::Quaternion', 'tf2::Quaternion'),
    ('ros::NodeHandle nh;', 'rclcpp::Node node();'),
    ('ros::Rate', 'rclcpp::Rate'),
    ('ros::ok', 'rclcpp::ok'),
    ('ros::init', 'rclcpp::init'),
    ('ros::spin', 'rclcpp::spin'),
    ('ros::Timer ', 'rclcpp::TimerBase::SharedPtr '),
    ('tf2::StampedTransform', 'geometry_msgs::msg::TransformStamped'),
    ('\.publish', '->publish'),
    ('boost::bind', 'std::bind'),
]

cmake_regex = [
    (r'cmake_minimum_required\(VERSION (.*)\)', r'cmake_minimum_required(VERSION 3.5)'),
]

launch_regex = [
    # type换成exec
    (r'(\<node.*?)type=(.*/?\>)', r'\1exec=\2'),
    # 去掉node里面的required
    (r'(\<node.*?) required\=\"\w*?\"(.*/?\>)', r'\1\2'),
    # 去掉param里面的type
    (r'(\<param.*?) type=\"\w*?\"(.*/?\>)', r'\1\2'),
    (r'\$\(find ', '$(find-pkg-share '),
    (r'\bns\b', 'namespace'),
    (r'\$\(arg', '$(var'),
    ('pkg\=\"rviz\"', 'pkg=\"rviz2\"'),
    ('name\=\"rviz\"', 'name=\"rviz2\"'),
    ('exec\=\"rviz\"', 'exec=\"rviz2\"'),
]

if __name__ == '__main__':
    # 如果argc < 2, 则运行测试代码,否则读取argv[1]文件替换文件内容
    open_file = len(sys.argv) > 1

    if open_file:
        text = open(sys.argv[1]).read()
    else:
        text = "const sensor_msgs::PointCloud2ConstPtr& laserCloud2"

    regex_pairs = []
    # 根据文件后缀选择正则表达式
    if open_file and (sys.argv[1].endswith('.cpp') or sys.argv[1].endswith('.h') or sys.argv[1].endswith('.hpp')):
        print('cpp file')
        regex_pairs = cpp_regex
    elif open_file and (sys.argv[1].endswith('.cmake') or sys.argv[1].endswith('CMakeLists.txt')):
        print('cmake file')
        regex_pairs = cmake_regex
    elif open_file and sys.argv[1].endswith('.launch'):
        print('launch file')
        regex_pairs = launch_regex

    for find, replace in regex_pairs:
        text = re.sub(find, replace, text)

    # 保存文件
    if open_file:
        with open(sys.argv[1], 'w') as f:
            f.write(text)
    else:
        print(text)

参考

Migrating from ROS 1 to ROS 2