欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

思岚激光雷达rplidar从ROS 1到ROS 2的移植

程序员文章站 2022-07-14 21:14:59
...


前言

由于需要将ROS 1下项目升级到ROS 2,用到了思岚的激光雷达,但目前似乎思岚官方还没有在ROS 2上的package,因此做了一个移植。移植过程参考了ROS 2官方的说明,尽最大可能按照ROS 2新架构的规范做移植,希望本文能对同样在ROS 2上做开发的同仁起到帮助


1. 创建rplida_ros的package

在终端输入命令:
cd ~/dev_ws/src
进入workspace的src目录,继续在终端输入命令:
ros2 pkg create --build-type ament_cmake rplidar_ros2 --dependencies rclcpp sensor_msgs std_srvs
package创建后打开其“package.xml”,参照ROS中rplidar的package.xml文件修改必要的信息,比如版本号,能够帮助查看是否需要后续的更新,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

2. 拷贝思岚rplidar的sdk以及rivz包

拷贝到新建package rplidar_ros2的根目录下,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
进入sdk目录,复制“node.cpp”为“node_ros2.cpp”作为ROS 2下的源码文件,同时保留了ROS下的“node.cpp”作为备份,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

3. 为CMakeLists添加rplidar sdk和node的目标编译文件,以及ROS 2 run命令在寻找node时的路径

打开package的“CMakeLists.txt”,在“find_package”前添加如下语句:

set(RPLIDAR_SDK_PATH "./sdk/")

FILE(GLOB RPLIDAR_SDK_SRC 
  "${RPLIDAR_SDK_PATH}/src/arch/linux/*.cpp"
  "${RPLIDAR_SDK_PATH}/src/hal/*.cpp"
  "${RPLIDAR_SDK_PATH}/src/*.cpp"
)

即指定rplidar sdk路径的变量“RPLIDAR_SDK_PATH”,同时将rplidar sdk所需要的文件glob到变量“RPLIDAR_SDK_SRC”,便于后续添加目标。关于cmake的glob的说明参考官网链接:
https://cmake.org/cmake/help/v3.0/command/file.html

在“find_package”后添加如下语句:

add_executable(rplidarNode src/node_ros2.cpp ${RPLIDAR_SDK_SRC})
ament_target_dependencies(rplidarNode rclcpp sensor_msgs std_srvs)

install(TARGETS
  rplidarNode
  DESTINATION lib/${PROJECT_NAME})

即为“rplidarNode”添加编译目标文件“node_ros2.cpp”、sdk文件、以及ROS 2 run命令查找node时的路径,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

4. 检查依赖问题

在终端输入命令:
rosdep install -i --from-path src --rosdistro foxy -y
如果依赖都已正确设置或安装,则如下图提示:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
否则,在终端运行命令:
rosdep update
更新并安装必要的依赖

5. 尝试编译,以解决CMakeLists问题

在终端输入命令:
colcon build --packages-select rplidar_ros2
编译首先提示找不到“ros/ros.h”,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
因为在ROS 2架构下变为了“rclcpp/rclcpp.hpp“,同理,后面的两个#include,即“sensor_msgs/LaserScan.h”和“std_srvs/Empty.h”,也分别变为了“<sensor_msgs/msg/laser_scan.hpp>”和“<std_srvs/srv/empty.hpp>”,也都需要替换,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
思岚激光雷达rplidar从ROS 1到ROS 2的移植
在终端再次输入编译命令:
colcon build --packages-select rplidar_ros2
此时编译提示找不到“rplidar.h”,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
该文件属于rplidar的头文件,在sdk/include目录下,这个问题表明CMakeLists缺少了include directories的声明,需要添加。打开package的“CMakeLists.txt”,在“install”后添加如下语句:

target_include_directories(rplidarNode
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/sdk/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/sdk/src>
    $<INSTALL_INTERFACE:include>)

即加入rplidar sdk的include和src两个路径,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
NOTE:关于ROS 2的ament_cmake说明可以参看官网链接:
https://index.ros.org/doc/ros2/Tutorials/Ament-CMake-Documentation/

6. 适应ROS 2架构的代码移植

ROS 2相比ROS在构建架构上做出了很大的变化,即引入了DDS(Data Distribution Service)来优化ROS中通信,关于DDS的引入的考虑可以参考官网链接:
https://design.ros2.org/articles/ros_on_dds.html

由于DDS的引入,代码由ROS到ROS 2最大的变化在于node的引用方式上,本文对rplidar代码的移植参考了ROS 2说明中的三个示例,Writing a simple publisher and subscriber (C++)、Writing a simple service and client (C++)、Using parameters in a class (C++),它们的官网链接分别为:
https://index.ros.org/doc/ros2/Tutorials/Writing-A-Simple-Cpp-Publisher-And-Subscriber/
https://index.ros.org/doc/ros2/Tutorials/Writing-A-Simple-Cpp-Service-And-Client/
https://index.ros.org/doc/ros2/Tutorials/Using-Parameters-In-A-Class-CPP/

首先,需要从rclcpp::Node继承我们自己的node,本文中为“rplidarROS2”。这里重点介绍继承类rplidarROS2的构造函数,因为该函数的实现过程几乎涵盖了ROS到ROS 2整个代码移植的关键变化,以及由此带来node的main函数的变化

首先看继承类rplidarROS2及其构造函数:

class rplidarROS2 : public rclcpp::Node
{
public:
	rplidarROS2()
		: Node("rplidar_ros2")
		, driver_(nullptr)
		, clock_(RCL_ROS_TIME)
		, angle_compensate_multiple_(1) // it stand of angle compensate at per 1 degree
		, frequency_(5.5)
		, channel_type_("serial")
		, tcp_ip_("192.168.0.7")
		, tcp_port_(20108)
		, serial_port_("/dev/ttyUSB0")
		, serial_baudrate_(115200/*256000*/) // ros run for A1 A2, change to 256000 if A3;
		, frame_id_("laser_frame")
		, inverted_(false)
		, angle_compensate_(false)
		, scan_mode_(std::string())
		, max_distance_(8.0)
		, screened_begin_(91)
		, screened_end_(179)
	{
		RCLCPP_INFO(get_logger(), "RPLIDAR running on ROS package rplidar_ros. SDK Version:"RPLIDAR_SDK_VERSION"");
		
		declare_parameters(); // !!!NOTE
		get_parameters();
		
		rclcpp::QoS qos(rclcpp::KeepLast(50));  // !!!NOTE
		scan_publisher_ = create_publisher<sensor_msgs::msg::LaserScan>("scan", qos);  // !!!NOTE
		start_motor_server_ = create_service<std_srvs::srv::Empty>("start_motor", std::bind(&rplidarROS2::start_motor, this, std::placeholders::_1, std::placeholders::_2));  // !!!NOTE
		stop_motor_server_ = create_service<std_srvs::srv::Empty>("stop_motor", std::bind(&rplidarROS2::stop_motor, this, std::placeholders::_1, std::placeholders::_2));
		
		timer_ = create_wall_timer(std::chrono::milliseconds(int(ceil(1000.0 / frequency_))), std::bind(&rplidarROS2::spin, this));  // !!!NOTE
		
		connect_driver();
		check_scan_mode();
	};
	
	// other codes but screened for space reason
}

该代码片段即为继承于rclcpp::Node的rplidarROS2类及其构造函数,其他部分未给出,将在后面的源代码中一并给出。本文出于尽最大可能保证所移植代码与rplidar在ROS下的代码一致或相似的目的,将rplidarROS2类作为inline的方式写入了同一个文件“node_ROS2.cpp”,其也可以写入独立的.h和.cpp文件

该代码片段中带有“ // !!!NOTE”注释的部分是需要着重关注的变化点:

a. declare_parameters()

该函数的源代码如下:

void declare_parameters()
{
	declare_parameter<double>("frequency", frequency_);
	declare_parameter<std::string>("channel_type", channel_type_);
	declare_parameter<std::string>("tcp_ip", tcp_ip_);
	declare_parameter<int>("tcp_port", tcp_port_);
	declare_parameter<std::string>("serial_port", serial_port_);
	declare_parameter<int>("serial_baudrate", serial_baudrate_);
	declare_parameter<std::string>("frame_id", frame_id_);
	declare_parameter<bool>("inverted", inverted_);
	declare_parameter<bool>("angle_compensate", angle_compensate_);
	declare_parameter<std::string>("scan_mode", scan_mode_);
	declare_parameter<double>("max_distance", max_distance_);
	declare_parameter<int>("screened_begin", screened_begin_);
	declare_parameter<int>("screened_end", screened_end_);
}

ROS 2从版本Dashing,就要求在客户端读取或者设置参数时首先需要声明参数,该变化的好处是客户端代码能够通过自身对参数的声明以此判断ParameterDescriptor中定义的参数是否合法。因为在ROS 2中已经没有global parameter的概念,所有变量都基于其node,因此有对node变量的ParameterDescriptor。同时,ROS 2也兼容了不声明参数的方式,通过在node构造函数中设置变量“allow_undeclared_parameters”以及“automatically_declare_parameters_from_overrides”为“true”来实现直接读取或设置参数的功能,可以参考下面的示例程序链接:
https://github.com/ros2/demos/blob/75cf923f9f3a33be456db1785f91e503c4bee16d/demo_nodes_cpp/src/parameters/parameter_blackboard.cpp#L29
关于声明参数的说明可以参考官网链接:
https://index.ros.org/doc/ros2/Releases/Release-Dashing-Diademata/#declaring-parameters

b. rclcpp::QoS qos(rclcpp::KeepLast(50))和scan_publisher_ = create_publisher…

该代码是因为引入DDS后的变化之一。ROS的通信基于TCP,引入了DDS后,ROS 2的通信就基于了UDP,由于UDP不像TCP是可靠连接,因此DDS通过QoS(Quality of Service)配置机制,依据不同的配置能够在UDP基础上实现与TCP类似的可靠连接

回想在ROS中发布消息的过程,以ROS rplidar为例:

ros::Publisher scan_pub = nh.advertise<sensor_msgs::LaserScan>("scan", 1000);

该代码两个参数topic和队列缓存数量1000,在ROS 2的QoS中可以通过组合配置“History”和“Depth”两个原则组合实现,即代码中的rclcpp::KeepLast(50)。详细的QoS配置及其说明,参考官网链接:
https://index.ros.org/doc/ros2/Concepts/About-Quality-of-Service-Settings/

NOTE:ROS rplidar发布消息队列缓存数量设置为了1000,不知道是出于什么原因,按经验来看,50就足够了

c. start_motor_server_ = create_service…和stop_motor_server_ = create_service…

由于使用了类及类成员函数的方式,start motor server和stop motor server两个node service的callback函数使用了bind以及用于表明callback形参数量的placeholders。也可以将callback函数定义为static静态类型,直接输入callback函数的地址。本文中由于callback需要访问类成员变量,static函数会引入额外的重构,因此采用了bind方式

d. timer_ = create_wall_timer…

函数create_wall_timer依据形参period的时间,周期性的调用给定的callback函数spin,本文考虑到rplidar激光扫描频率的可变性并能适配不同频率的rplidar产品,将period设置为参数变量,即可以通过launch方式设置

回想ROS rplidar中main函数while循环处理扫描的过程,其调用了rplidar驱动中的函数“grabScanDataHq”,该函数是一个同步函数,每一次扫描处理的时间决定于rplidar的扫描频率,比如5.5Hz即为182ms左右,因此wall timer调用spin函数的周期,设置为基于launch输入参数的频率计算得到的时间周期

本文在wall timer中调用了spin函数,该函数即是ROS rplidar中main函数的while循环部分。因为spin本文也设置为类成员函数,因此同样使用了bind方式的callback调用

最后,在引入了node继承类后,main函数会变得非常简洁,下面为main函数代码:

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);  // !!!NOTE
   
	try
	{
		auto node = std::make_shared<rplidarROS2>();  // !!!NOTE
		rclcpp::spin(node);  // !!!NOTE
		rclcpp::shutdown();
	}
	catch (const std::runtime_error& error)
	{
		printf((std::string(error.what()) + "\n").c_str());
		return -1;
	}
	
    return 0;
}

main函数和ROS一致的地方就是进行初始化,区别在ROS 2中变为了rclcpp::init(argc, argv)。同时因为继承类rplidarROS2定义了所有关于激光雷达的操作,main函数只需要创建rplidarROS2的node,即auto node = std::make_shared(),并开启node的spin即可,即rclcpp::spin(node)。整个main函数变得非常简洁直观,这也是ROS 2架构带来的一大变化

7. 再次编译移植后代码

保存移植后代码即node_ROS2.cpp,在终端再次输入命令:
colcon build --packages-select rplidar_ros2
一切顺利的化,终端将输出编译完成的信息,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
在终端继续输入命令:
. install/setup.sh
将package加入setup bash

NOTE:由于本文基于最大可能保持与ROS rplidar的一致性,便于后续如果思岚官方有更新时能够快速merge,因此没有处理编译warning

在终端继续输入命令:
ros2 run rplidar_ros2 rplidarNode
激光雷达node将运行,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
NOTE:第一次运行时可能会出现提示“Error, cannot bind to the specified serial port /dev/ttyUSB0.”,可以通过下面命令:
sudo gpasswd --add ${USER} dialout
重启后解决

激光雷达运行后,可以通过topic echo命令查看/scan消息,检验移植的正确性。打开一个新终端输入命令:
ros2 topic echo /scan
将输入当前激光雷达发布的消息/scan,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
TIP:通过命令ros2 topic list查看所有运行node发布的消息,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

8. launch

代码移植后可以通过命令ros2 run命令运行,但通常在ROS下需要组合多个node运行,比如navigation,因此还需要launch方式实现。ROS 2引入了python代码的launch方式,详细内容以及设计思想参考官网链接:
https://index.ros.org/doc/ros2/Tutorials/Launch-system/
https://design.ros2.org/articles/roslaunch.html

但同时,ROS 2也兼容ROS下xml的launch文件,但做了部分修改,参考官网链接:
https://index.ros.org/doc/ros2/Tutorials/Launch-files-migration-guide/

本文采用第二中方式,即基于ROS下已有的xml launch文件依据ROS 2做适配性修改

a. 为package rplidar_ros2建立launch文件夹

进入rplidar_ros2的根目录,新建文件夹lauch,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

b. 拷贝ROS rplidar的launch文件

在rplidar中特定产品的运行都有两个对应的launch文件,比如产品A1的launch文件为“rplidar.launch”和“view_rplidar.launch”,本文将以这两个文件做说明,其他产品的处理方式是一致的

拷贝ROS rplidar\launch目录下的文件“rplidar.launch”和“view_rplidar.launch”到ROS 2的rplidar_ros2\launch目录下,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

c. rplidar.launch的ROS 2 launch适配修改

打开rplidar.launch修改为下面语句:

<launch>
  <node name="rplidarNode" pkg="rplidar_ros2" exec="rplidarNode" output="screen">
    <param name="frequency" value="5.5"/>
    <param name="serial_port" value="/dev/lidar"/>
    <param name="serial_baudrate" value="115200"/><!--A1/A2 -->
    <!--param name="serial_baudrate" value="256000"--><!--A3 -->
    <param name="frame_id" value="laser"/>
    <param name="inverted" value="false"/>
    <param name="angle_compensate" value="true"/>
    <param name="scan_mode" value="Standard"/>
    <param name="screened_begin" value="101"/>
    <param name="screened_end" value="259"/>
    <param name="max_distance" value="8.0"/>
  </node>
</launch>

ROS 2中node没有了type属性,变为了“exe”属性,param没有了type属性,变为依据用户填入数据自动判断,详细区别参考官网链接:
https://index.ros.org/doc/ros2/Tutorials/Launch-files-migration-guide/

保存修改后在终端输入命令:
ros2 launch src/rplidar_ros2/launch/rplidar.launch
激光雷达node将运行,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
NOTE:仔细观察run命令和launch命令的两种方式会发现“Point number”的数量分别为4.0K和2.0K。这是由于launch运行使用了我们指定的参数而得到不同的激光雷达配置结果,具体区别是“angle_compensate”以及“scan_mode”,可以回想在构造函数中上述两个参数与rplidar.launch中默认值及设定值的区别

d. 添加package的xml launch文件

仔细回想ROS中launch的调用方式:roslaunch package_name target.launch,对比上述运行步骤中命令ros2 launch path/target.launch,会发现ROS 2中运行launch不再需要指定package,直接给出目标launch的路径即可,这是ROS 2 launch system带来的另一个变化。有时候也许launch文件的路径不容易记忆,通过package和TAB键能帮助联想出目标launch文件的方式依然具有优势,ROS 2兼容了该方式,可以通过CMakeLists文件中实现

打开rplidar_ROS2根目录下的“CMakeLists.txt”文件,在文件最后但“ament_package()”之前,增加如下语句:

# Install launch files.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)

在终端输入命令:
cd ~/dev_ws
colcon build --packages-select rplidar_ros2

重新编译rplidar_ros2,继续在终端输入命令:
. install/setup.sh
将package加入setup bash。此时通过终端输入package和其target.launch的命令:
ros2 launch rplidar_ros2 rplidar.launch
即可通过tab联想目标launch,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

e. view_rplidar.launch以及rplidar.rviz的ROS 2 launch适配修改

打开view_rplidar.launch修改为下面语句:

<!--
Used for visualising rplidar in action.
It requires rplidar.launch.
-->
<launch>
  <group>
    <!--<push-ros-namespace namespace="rplidar_ros2"/> with user defined ns-->
    <include file="/home/whi/dev_ws/src/rplidar_ros2/launch/rplidar.launch"/>
  </group>

  <node name="rviz2" pkg="rviz2" exec="rviz2" args="-d /home/whi/dev_ws/src/rplidar_ros2/rviz/rplidar.rviz"/>
</launch>

ROS 2中include标签必须被group标签包含,同时动态的$(find rplidar_ros)被取消,因此这里输入了绝对路径

NOTE:在ROS 2 Design中关于动态配置有介绍built-in的$(find-pkg-prefix )但本文尝试未能成功

另外需要修改的是rviz目录下的rplidar.rviz,ROS 2中rviz也升级到了rviz 2,大部分configuration也发生了相应的变化,因此本文不在之前ROS下的rviz上做修改,而是通过手动过程在rviz中添加相应的frame和plugin,并另存rviz2下的configuration文件

在终端运行刚才修改的view_rplidar.launch,输入命令:
ros2 launch src/rplidar_ros2/launch/view_rplidar.launch
运行后可以看到终端输出激光雷达成功运行,并打开了rviz,但rviz为默认初始配置,不能显示激光雷达点云

首先将“Fixed Frame”由map改为laser;再添加“LaserScan”插件,并将“topic”设置为/scan,此时rviz的显示区域将能显示激光雷达的点云,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
此时的点云不明显,把LaserScan插件的三个属性“Size”,“ColorTransform”,“Axis”分别设置为“0.03”,“AxisColor”,“Z”,此时点云将变得更明显,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
点击菜单“File->Save Config As”,存储该配置到rviz目录,命名为rplidar.rviz,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植
关闭终端中运行的launch,再次运行view_rplidar.launch,在终端输入命令:
ros2 launch src/rplidar_ros2/launch/view_rplidar.launch
此时将看到激光雷达成功运行,以及我们需要的rviz显示,如下图:
思岚激光雷达rplidar从ROS 1到ROS 2的移植

总结

至此,思岚激光雷达从ROS到ROS 2的移植工作已经完成,为了支持思岚激光雷达的其他系列产品,可以依据上述步骤继续修改其他launch文件

本文的移植代码已经从源代码端考虑了不同产品频率的兼容性,即通过设置变量“frequency”实现不同产品频率的配置,在移植其他产品launch文件时需要关注并检查该参数变量是否符合产品特性设置

同时,本文的移植代码加入了扫描角度的屏蔽范围设置,即参数“screened_begin”和“screened_end”

本文源代码的github地址:
https://github.com/xinjuezou-whi/rplidar_ros2

相关标签: ROS 2 c++