Category Archives: ROS/ROS2

ROS2_Foxy学习9——多机激光SLAM先导篇

  这篇文章的整体目的是在多个车上跑slam,通过ROS2_Foxy将地图集中到一个电脑下,然后拼接成一张地图。
  一开始的想法是车上用ROS2跑SLAM,由于各大开源SLAM算法对ROS2不支持或者支持不完善(又因为 能力不够,不能把代码从ROS1移植到ROS2),因而,使用ROS1跑SLAM,然后通过ROS2的包ros1_bridge在各车与电脑之间传递数据,从而实现此项目。
  在引入ros1_bridge之前,先写一写在ROS1上跑开源SLAM,以及在ROS2上跑开源SLAM的一些过程。
  因为没做完 内容较多,所以分了几个部分来写,这一篇是探索可行性的。

1 环境准备

设备 处理器 系统 ROS1版本 ROS2版本
电脑 amd64_酷睿i7 5500U_内存8GB Ubuntu 20.04 LTS Noetic Foxy
arm64_树莓派4B_内存4GB Ubuntu mate 20.04 LTS Foxy

  激光雷达使用思岚RPLidarA1,由于这个雷达频率较低,因此开源SLAM选择cartographer。没有选择Hector的原因除了RPLidarA1扫描频率太低,用Hector效果不理想外,Hector官方没有给ROS2的版本,自己移植太费时间。

2 ROS1下测试SLAM

2.1 cartographer 源码测试

  参考官方文档:https://google-cartographer-ros.readthedocs.io/en/latest/compilation.html

# 安装工具
    sudo apt-get update
    sudo apt-get install -y python3-wstool python3-rosdep ninja-build stow

# 下载源码
    mkdir catkin_ws
    cd catkin_ws
    wstool init src
    wstool merge -t src https://raw.githubusercontent.com/cartographer-project/cartographer_ros/master/cartographer_ros.rosinstall
    wstool update -t src

# 安装依赖
    sudo rosdep init
    rosdep update
    rosdep install --from-paths src --ignore-src --rosdistro=${ROS_DISTRO} -y

# 手动安装abseil-cpp库
    src/cartographer/scripts/install_abseil.sh

# 编译安装
    . /opt/ros/noetic/setup.bash    #(记得source ros1的安装位置)
    catkin_make_isolated --install --use-ninja

# 跑个包
    . install_isolated/setup.bash       #(记得source cartographer的安装位置)
    wget -P ~/Downloads https://storage.googleapis.com/cartographer-public-data/bags/backpack_2d/cartographer_paper_deutsches_museum.bag
    roslaunch cartographer_ros demo_backpack_2d.launch bag_filename:=${HOME}/Downloads/cartographer_paper_deutsches_museum.bag

  跑的bag大概是这样子:
在这里插入图片描述

2.2 rplidar_ros 源码测试

  参考官方文档:https://github.com/Slamtec/rplidar_ros

# 下载源码
    mkdir -p catkin_ws/src
    cd catkin_ws/src
    git clone https://github.com/Slamtec/rplidar_ros.git

# 编译
    . /opt/ros/noetic/setup.bash    #(记得source ros1的安装位置)
    catkin_make

# 连接雷达,并赋予设备权限(根据实际情况,选择设备名称)
    sudo chmod 777 /dev/ttyUSB0

# 跑个demo(记得source rplidar_ros的安装位置)
    . devel/setup.bash      # 因为只编译,没有安装,所以没有install文件夹
    roslaunch rplidar_ros test_rplidar.launch

  跑的是这样子:
在这里插入图片描述  再跑个rviz的demo

    roslaunch rplidar_ros view_rplidar.launch

  跑的是这样子:
在这里插入图片描述

2.3 rplidar_ros + cartographer

1、准备源码
  新建工作空间,并把cartographerrplidar_ros的源码包放到/src文件夹下。
2、编写cartographer的launch文件和lua配置文件
  直接参考源码给的demo来改,主要参考了cartographer_ros包下的launch/demo_revo_lds.launch及其对应的configuration_files/revo_lds.lua,改动的文件内容如下,这里是重新编写了两个文件,并将这两个新文件放在了跟参考文件同一个文件夹内:

launch文件:cartoForRplidar.launch <—— demo_revo_lds.launch

<launch>  
  <--!false 是因为要使用真实雷达数据,不用仿真时间 /-->
  <param name="/use_sim_time" value="false" />  

  <--!注意configuration_basename 后面的配置文件名,要与实际的lua文件名一致 /-->
  <node name="cartographer_node" pkg="cartographer_ros"  
        type="cartographer_node" args="  
            -configuration_directory $(find cartographer_ros)/configuration_files  
            -configuration_basename rplidar.lua"  
        output="screen">  
    <--!这句实际上,可以删掉 /-->
    <remap from="scan" to="scan" />  
  </node>  
  <node name="rviz" pkg="rviz" type="rviz" required="true"  
        args="-d $(find cartographer_ros)/configuration_files/demo_2d.rviz" />  
</launch>

lua文件:rplidar.lua <—— revo_lds.lua
  这个文件只改了tracking_frame和published_frame,这两个frame,一般是laser,需要根据雷达ros驱动的实际情况配置。

include "map_builder.lua"
include "trajectory_builder.lua"

options = {
  map_builder = MAP_BUILDER,
  trajectory_builder = TRAJECTORY_BUILDER,
  map_frame = "map",
  tracking_frame = "laser",
  published_frame = "laser",
  odom_frame = "odom",
  provide_odom_frame = true,
  publish_frame_projected_to_2d = false,
  use_pose_extrapolator = true,
  use_odometry = false,
  use_nav_sat = false,
  use_landmarks = false,
  num_laser_scans = 1,
  num_multi_echo_laser_scans = 0,
  num_subdivisions_per_laser_scan = 1,
  num_point_clouds = 0,
  lookup_transform_timeout_sec = 0.2,
  submap_publish_period_sec = 0.3,
  pose_publish_period_sec = 5e-3,
  trajectory_publish_period_sec = 30e-3,
  rangefinder_sampling_ratio = 1.,
  odometry_sampling_ratio = 1.,
  fixed_frame_pose_sampling_ratio = 1.,
  imu_sampling_ratio = 1.,
  landmarks_sampling_ratio = 1.,
}

MAP_BUILDER.use_trajectory_builder_2d = true

TRAJECTORY_BUILDER_2D.submaps.num_range_data = 35
TRAJECTORY_BUILDER_2D.min_range = 0.3
TRAJECTORY_BUILDER_2D.max_range = 8.
TRAJECTORY_BUILDER_2D.missing_data_ray_length = 1.
TRAJECTORY_BUILDER_2D.use_imu_data = false
TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.linear_search_window = 0.1
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.translation_delta_cost_weight = 10.
TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.rotation_delta_cost_weight = 1e-1

POSE_GRAPH.optimization_problem.huber_scale = 1e2
POSE_GRAPH.optimize_every_n_nodes = 35
POSE_GRAPH.constraint_builder.min_score = 0.65

return options

3、编译
  catkin_make和catkin_make_isolated都可以,区别是后者使用一个隔离的构建过程,其中每个包都是独立配置、构建和加载到环境中的,解决了目标冲突、目标依赖项管理和项目之间其他不希望的交叉对话的问题,缺点是比较慢,具体参考这篇博客
  tips:编译后再修改launch或lua文件,只编译单独的包即可

    catkin_make_isolated --pkg cartographer_ros

4、跑一跑
  先跑cartographer再跑rplidar_ros,如果反了,似乎不太行,原因还没有深究。

# 首先的首先,配置环境变量
    . /opt/ros/noetic/setup.bash
    . devel_isolated/setup.bash     # 因为只编译,没有安装,所以没有install文件夹

# 首先运行 cartographer
    roslaunch cartographer_ros cartoForRplidar.launch

# 然后运行 rplidar_ros
    roslaunch rplidar_ros test_rplidar.launch

  出图:
在这里插入图片描述

3 ROS2下测试SLAM

3.1 cartographer 安装测试(失败)

  ROS2_Foxy上使用cartographer有两种方式:命令行安装和源码编译安装。

# 命令行
    sudo apt install ros-foxy-cartographer
#源码
    https://github.com/cartographer-project/cartographer_ros/tree/ros2-dashing

  cartographer似乎正在开发一个新版本(点击了解详情),直到去年11月还在开发中…

3.2 rplidar_ros 移植到 ROS2

  参考这里:ros2激光雷达功能包移植(思岚rplidar A1)
1、下载代码,然后找到rplidar_ros的包,并拷贝到自己的工作空间的src目录下

    git clone https://github.com/DylanLN/oryxbot_ws-ros2.git

2、编译、测试

# 编译
    . /opt/ros/foxy/setup.bash
    colcon build

# 跑一跑
    . install/setup.bash
    sudo chmod 777 /dev/ttyUSB0 
    ros2 launch rplidar_ros rplidar_A1.launch.py    

  另开一个终端,跑rviz2

    . /opt/ros/foxy/setup.bash
    . install/setup.bash
    rviz2

  出图:
在这里插入图片描述因为cartographer还没有成功在ROS2上跑,所以这里没有跑slam,等后面官方移植完善了,再看看。

4 ROS1 & ROS2下测试SLAM

  好了,现在到了先导篇,最奇怪的一部分了:在ROS2上跑rplidar_ros,然后通过ros1_bridge把激光数据传到ROS1,然后在ROS1中运行cartographer进行建图。
  按照前言中讲述的内容,激光雷达与cartographer都在ROS1环境下,车辆与电脑之间应该传递的是单车slam建图的结果,那么这里为什么把激光雷达放到ROS2下,然后在ROS1中接收ROS2的激光数据进行建图呢?因为不想在树莓派上,装完ROS2,再装ROS1,所以车子的作用变成了只采集激光数据,然后通过ROS2传到电脑,电脑端在ROS1中进行SLAM。
  也许,后面会尝试在树莓派上再装个ROS1?

4.1 ros1_bridge

1、小前言
  ros1_bridge是ROS2为了兼容ROS1而开发的包,这里介绍下基本的使用方法,然后将其应用到项目中。学习就是看看官网教程,这个包的安装过程可以命令行直接安装。

    sudo apt install ros-foxy-ros1-bridge

2、跑一跑(要开四个终端)

# Shell A (ROS 1 only):
. /opt/ros/noetic/setup.bash
roscore

# Shell B (ROS 1 + ROS 2):
# Source ROS 1 first:
. /opt/ros/noetic/setup.bash
# Source ROS 2 next:
. /opt/ros/foxy/setup.bash
export ROS_MASTER_URI=http://localhost:11311    # 这里根据 Shell A 实际情况改
ros2 run ros1_bridge dynamic_bridge

# Shell C:
. /opt/ros/noetic/setup.bash
rosrun rospy_tutorials talker

# Shell D:
. /opt/ros/foxy/setup.bash
ros2 run demo_nodes_cpp listener

  出图:
在这里插入图片描述

4.2 ROS1 & ROS2 & cartographer

1、ROS1下,跑cartographer,这会同时启动roscore

    . /opt/ros/noetic/setup.bash
    . devel_isolated/setup.bash
    roslaunch cartographer_ros cartoForRplidar.launch

在这里插入图片描述2、ROS1和ROS2下,跑ros1_bridge

    . /opt/ros/noetic/setup.bash
    . /opt/ros/foxy/setup.bash
    export ROS_MASTER_URI=http://localhost:11311    
    ros2 run ros1_bridge dynamic_bridge

3、ROS2下,跑rplidar_ros

    . /opt/ros/foxy/setup.bash
    . install/setup.bash
    ros2 launch rplidar_ros2 rplidar_A1.launch.py
    # 这个包名叫rplidar_ros2而非rplidar_ros,是因为修改了launch文件,'frame_id': 'laser'
    # 以便于在下一部分与cartographer的frame对接正确

4、出图
  可以看到,ROS1的cartographer拿到数据正在建图,第二张图右上角的终端显示两个ROS正在通过话题sensor_msgs/LaserScan通信。
在这里插入图片描述在这里插入图片描述

参考

1、ROS(Kinetic和Melodic)下使用cartographer踩坑记录和部分谣言终结
2、catkin_make, cmake, catkin build区别
3、ros2激光雷达功能包移植(思岚rplidar A1)

ROS2_Foxy学习7——构建

  官方文档总是很丰富,关于构建工具,参考A universal build toolUsing colcon to build packagesament_cmake user documentation

1 前言

  ROS生态中,一个软件是由多个分离的package组成的,运行软件,首先需要构建(build)这些package。
  手动构建一组package的方法就是逐个构建,每个package都有特定的构建系统,需要根据依赖情况设置环境,然后build。
  如果package及其依赖较多的话,其效率是难以接受的,这就需要一个自动化的构建工具,只需要调用一次即可构建一组package。
  ROS1提供了多个自动构建工具,catkin_make, catkin_make_isolated, 和 catkin_tools。ROS2提供了一个自动构建工具,ament_tools。

2 构建工具与构建系统

2.1 构建工具

  构建工具在一组package上运行,它确定依赖关系图,并以拓扑顺序为每个包调用特定的构建系统。构建工具只需要知道每个包特定的构建系统是如何设置环境以构建和使用包的即可。
  构建工具例如catkin_make,catkin_make_isolated,catkin_tools,ament_tools,colcon。

2.2 构建系统

  构建系统在单个package上运行。
  构建系统例如Make,CMake,Python setuptools,ament等,ROS的catkin和ament_cmake基于CMake的。
  (一般使用包括以下步骤:cmake, make, make install)

3 构建工具 colcon

3.1 colcon背景

  colcon是ROS构建工具catkin_make、catkin_make_isolated、catkin_tools和ament_tools的迭代。colcon目标是做一个通用构建工具(universal build tool),能够构建ROS1和ROS2的包,同时也能够构建一些非ROS包。ROS中,catkin_make、catkin_make_isolated、catkin_tools、ament_tools将逐步被colcon取代。

3.2 colcon安装

$ sudo apt install python3-colcon-common-extensions

3.3 colcon使用

1、创建工作空间

$ mkdir -p ~/ros2_example_ws/src
$ cd ~/ros2_example_ws

注:-p 的意义

2、下载示例代码到src文件夹,并检查兼容性

$ git clone https://github.com/ros2/examples src/examples
# check
$ cd ~/ros2_example_ws/src/examples/
$ git checkout $ROS_DISTRO
$ cd ~/ros2_example_ws

  完成后,工作空间的文件组织结构如下

.
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

3、在工作空间的根目录编译示例代码

$ cd ~/ros2_example_ws
$ colcon build --symlink-install
# --symlink-install* 的意义么看懂?不加,也能编译成功。

  完成后,工作空间组织结构如下

.
├── build
├── install
├── log
└── src
    └── examples
        ├── CONTRIBUTING.md
        ├── LICENSE
        ├── rclcpp
        ├── rclpy
        └── README.md

4、测试示例代码

$ colcon test

5、配置环境变量,然后运行节点

$ . install/setup.bash
$ ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function

# 打开另一个终端
$ . install/setup.bash
$ ros2 run examples_rclcpp_minimal_publisher publisher_member_function

3.4 tips

1、COLCON_IGNORE:在不需要编译的包中添加文件COLCON_IGNORE,colcon将忽略这一功能包
2、colcon支持多种编译类型,推荐ament_cmake 和 ament_python,同时也支持纯cmake包。(在使用ros2 pkg create命令创建功能包时,可以使用–build-type参数来指定该功能包的编译类型,然后按模板生成功能包)

$ touch COLCON_IGNORE

3.5 colcon_cd命令

  colcon_cd命令的目的是从当前目录,换成某个已有的package目录。
1、colcon_cd安装
  参考官方,通过命令行安装二进制文件

$ sudo sh -c 'echo "deb [arch=amd64,arm64] http://repo.ros2.org/ubuntu/main <code>lsb_release -cs</code> main" > /etc/apt/sources.list.d/ros2-latest.list'
$ curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add -
$ sudo apt update
$ sudo apt install python3-colcon-common-extensions

2、colcon_cd使用

# 获取安装文件
$ echo "source /usr/share/colcon_cd/function/colcon_cd.sh" >> ~/.bashrc
# 提前指定包的位置
$ echo "export _colcon_cd_root=~/ros2_install" >> ~/.bashrc
# 目录变换到 ~/ros2_install 工作空间下的 some_ros_package 包
$ colcon_cd some_ros_package

4 构建系统 ament

  ament构建系统包括两个基本的子类,ament_python和ament_cmake,这里只讨论ament_cmake。
  使用ament_cmake编译系统的功能包会对应生成两个文件package.xml和CMakeLists.txt。

4.1 编写 package.xml

  package.xml描述了功能包的发行信息、依赖信息等,内容如下。
  在自动创建后,需要手动修改功能包描述、功能包版本、功能包许可证信息等发行信息。以“ _depend”结尾的标签用来描述功能包依赖,在增加新的依赖时,需要添加到其中。

<!--  功能包基础内容,不用修改   -->
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<!--  下面是功能包的信息  -->
<package format="3">
    <!--  包名,新建的时候就定了,不改  -->
    <name>cpp_srvcli</name>
    <!--  关于发行信息   -->
    <version>0.0.0</version>
    <description>这里写功能描述</description>
    <maintainer email="这里写邮箱地址">Your Name</maintainer>
    <license>这里写许可证</license>
    <!--  编译系统的依赖   -->
    <buildtool_depend>ament_cmake</buildtool_depend>
    <!--  程序所需的依赖,根据需要修改添加   -->
    <depend>rclcpp</depend>
    <!--  编译系统,新建功能包的时候可以指定   -->
    <export>
    <build_type>ament_cmake</build_type>
    </export>
</package>

4.2 编写 CMakeLists.txt

1、基本框架

cmake_minimum_required(VERSION 3.5) //cmake版本要求
project(my_project)                 //功能包名,与同一功能包下package.xml中名字一样

ament_package()                     //配置project,只调用一次,且在最后

ament_package()的作用有这么几个:
第一,安装package.xml文件,ament_package()提供了查找和解析package.xml文件的API;
第二,按ament索引,注册功能包;
第三,安装cmake配置文件(也就是生成.cmake文件,名字类似于FindXXX.cmake或者xxxConfig.cmake)以便于其他包要依赖这个包时,能被cmake找到。

2、编译目标
  编译目标包括 librariesexecutables

  对于编译库,cmake语法如下。

# A 创建库,并指定编译时的源文件
    add_library(<name> [STATIC | SHARED | MODULE]
                [EXCLUDE_FROM_ALL]
                source1 [source2 ...])
    # STATIC : 静态库
    # SHARED : 共享库
    # MODULE : 在使用 dyld 的系统有效,如果不支持 dyld,则被当作 SHARED 对待

# B 编译这个库时要包含的头文件,必须先使用add_library创建
    target_include_directories(<name> <INTERFACE|PUBLIC|PRIVATE>
                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                $<INSTALL_INTERFACE:include>)
    # CMAKE_CURRENT_SOURCE_DIR :当前处理的CMakeLists.txt所在的目录
    # BUILD_INTERFACE : 构建时的接口
    # INSTALL_INTERFACE :安装时的接口

# EXAMPLE
    add_library(action_server SHARED src/fibonacci_action_server.cpp)
    target_include_directories(action_server PRIVATE
                $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
                $<INSTALL_INTERFACE:include>)

  对于编译可执行文件,cmake语法如下。

# A 创建可执行文件,并指定编译时的源文件
    add_executable(<name> [WIN32] [MACOSX_BUNDLE]
                    [EXCLUDE_FROM_ALL]
                    [source1] [source2 ...])
    # WIN32 MACOSX_BUNDLE 分别是Windows和Mac系统的构建选项
    # EXCLUDE_FROM_ALL选项增加,表示不构建此可执行文件

# EXAMPLE
    add_executable(server src/add_three_ints_server.cpp)

3、编译依赖
  编译依赖有两种方式,ament_target_dependencies和target_link_libraries。
  首先推荐前者,自动包含头文件、库及其依赖项,使用覆盖工作区时,将确保正确排列所有依赖项的包含目录。
  当前者无法找到库时,使用后者。例如自定义的库文件,仍需要使用target_link_libraries的方式链接。语法类似,但需要包含必要的依赖和使用include_directories()包含必要的头文件。
  同样,目标要先创建,才能添加依赖。

# ament_target_dependencies
    find_package(dependence REQUIRED)
    ament_target_dependencies(target Eigen3)

# EXAMPLE
    find_package(rclcpp REQUIRED)
    ament_target_dependencies(action_server
      "rclcpp")

4、导出库
(这个,后面用到的时候再补充…)

    ament_export_targets(export_my_library HAS_LIBRARY_TARGET)
    ament_export_dependencies(some_dependency)

    install(
      DIRECTORY include/
      DESTINATION include
    )

    install(
      TARGETS my_library
      EXPORT export_my_library
      LIBRARY DESTINATION lib
      ARCHIVE DESTINATION lib
      RUNTIME DESTINATION bin
      INCLUDES DESTINATION include
    )

5、install命令

# A cmake的语法如下
    install(TARGETS targets... [EXPORT <export-name>]
            [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
              PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
             [DESTINATION <dir>]
             [PERMISSIONS permissions...]
             [CONFIGURATIONS [Debug|Release|...]]
             [COMPONENT <component>]
             [NAMELINK_COMPONENT <component>]
             [OPTIONAL] [EXCLUDE_FROM_ALL]
             [NAMELINK_ONLY|NAMELINK_SKIP]
            ] [...]
            [INCLUDES DESTINATION [<dir> ...]]
            )
# EXAMPLE        
    install(TARGETS
          action_server
          action_client
          ARCHIVE DESTINATION lib
          LIBRARY DESTINATION lib
          RUNTIME DESTINATION bin)
    # ARCHIVE 安装静态库到 ${CMAKE_INSTALL_LIBDIR}/lib
    # LIBRARY 安装动态库到 ${CMAKE_INSTALL_LIBDIR}/lib
    # RUNTIME 安装可执行二进制文件到 ${CMAKE_INSTALL_BINDIR}/bin

6、target_compile_options

附:除官网外的其他参考

1、https://cmake.org/cmake/help/v3.19/command/target_include_directories.html

2、ROS学习日记2

3、cmake(8):install命令详解

ROS2_Foxy学习6——进阶编程_C++

里面的例子参考官方教程,然后附带一些解释和一些推荐的便于理解的文章。

1 编写 action

新建工作空间,并新建两个功能包:动作消息功能包和动作功能包。动作消息功能包中编写action文件,然后在动作功能包中使用前者的action接口。

1.1 自定义消息 action

1、创建:在动作消息功能包(这里包名是action_tutorials_interfaces)下,创建/action文件夹(与/src同级),并在其中建立.action消息文件,有关消息内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

Fibonacci.action

int32 order
---
int32[] sequence
---
int32[] partial_sequence

2、在package.xml文件中添加

    <buildtool_depend>rosidl_default_generators</buildtool_depend>
    <depend>action_msgs</depend>
    <member_of_group>rosidl_interface_packages</member_of_group>

注:action_msgs必要。
3、在CMakeLists.txt文件中添加

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "action/Fibonacci.action"
)

4、消息文件测试
功能包编译后(没有节点,没有运行),可以通过命令ros2 interface show来查看消息文件。

$ ros2 interface show action_tutorials_interfaces/action/Fibonacci

:下面创建的动作功能包名是action_tutorials_cpp,附带以下依赖,其中第一个就是刚刚创建的自定义消息功能包,可以在创建包时,通过参数dependencies添加,也可以手动修改package.xml(< depend >)和CMakeLists.txt(find_package)。

action_tutorials_interfaces 
rclcpp 
rclcpp_action 
rclcpp_components

1.2 动作服务端 action server

在动作功能包的/src目录下,编写动作服务端节点,fibonacci_action_server.cpp

#include <functional>
#include <memory>
#include <thread>

#include "action_tutorials_interfaces/action/fibonacci.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"

//#include "action_tutorials_cpp/visibility_control.h"

namespace action_tutorials_cpp
{
class FibonacciActionServer : public rclcpp::Node
{
public:
    using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
    using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;

    //构造函数:
    //1、初始化动作服务节点名 fibonacci_action_server
    //2、初始化动作服务器:消息类型Fibonacci,所属节点this,动作名fibonacci,回调函数
    explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : Node("fibonacci_action_server", options)
    {
    using namespace std::placeholders;

    this->action_server_ = rclcpp_action::create_server<Fibonacci>( //消息类型Fibonacci
    this,                                   //所属节点this,
    "fibonacci",                                //动作名fibonacci
    std::bind(&FibonacciActionServer::handle_goal, this, _1, _2),   //回调函数:handle_goal      接受目标
    std::bind(&FibonacciActionServer::handle_cancel, this, _1),     //回调函数:handle_cancel    接受目标取消
    std::bind(&FibonacciActionServer::handle_accepted, this, _1));  //回调函数:handle_accepted  接受新目标并处理
    }

private:
    rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;

    //接受目标,同时告知client,这边知晓目标了,告知client的过程被自动处理了,下同
    rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const Fibonacci::Goal> goal)
    {
        RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order);
        (void)uuid;
        return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
    }

    //接受目标取消,同时告知client,这边知道取消了
    rclcpp_action::CancelResponse handle_cancel(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Received request to cancel goal");
        (void)goal_handle;
        return rclcpp_action::CancelResponse::ACCEPT;
    }

    //接受新目标并处理
    void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        using namespace std::placeholders;

        //因为处理过程是一个耗时且长期运行的操作,因此将其移到另一线程处理
        std::thread{std::bind(&FibonacciActionServer::execute, this, _1), goal_handle}.detach();
    }

    //处理并反馈
    void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle)
    {
        RCLCPP_INFO(this->get_logger(), "Executing goal");

        //处理频率 1Hz
        rclcpp::Rate loop_rate(1);

        //获取目标
        const auto goal = goal_handle->get_goal();

        //定义反馈
        auto feedback = std::make_shared<Fibonacci::Feedback>();
        auto & sequence = feedback->partial_sequence;
        sequence.push_back(0);  //斐波那契数列的前两个值是 0 1  动作服务器将不断反馈当前的sequence到客户端
        sequence.push_back(1);

        //定义结果
        auto result = std::make_shared<Fibonacci::Result>();

        //计算
        for (int i = 1; (i < goal->order) && rclcpp::ok(); ++i) 
        {
            //检查目标是否被取消
            if (goal_handle->is_canceling()) 
            {
                result->sequence = sequence;    
                goal_handle->canceled(result);  
                RCLCPP_INFO(this->get_logger(), "Goal canceled");
                return;
            }

            //更新sequence
            sequence.push_back(sequence[i] + sequence[i - 1]);

            //反馈sequence
            goal_handle->publish_feedback(feedback);
            RCLCPP_INFO(this->get_logger(), "Publish feedback");

            loop_rate.sleep();
        }

        //目标达成
        if (rclcpp::ok()) 
        {
            result->sequence = sequence;
            goal_handle->succeed(result);
            RCLCPP_INFO(this->get_logger(), "Goal succeeded");
        }
    }
};  // class FibonacciActionServer

}  // namespace action_tutorials_cpp

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionServer)


1、goal_handle:关于目标句柄goal_handle(也就是rclcpp_action::ServerGoalHandle),可以看下官方的帮助文档,这里刚学,还不是很理解,写多了也许就明白了。
2、std::thread:看这里,有关join和detach可以瞧一瞧这里
3、这里没有使用main函数,而是用下面这句,表明该节点可以在运行时动态加载,看看官方帮助文档,这里也不是很理解,后面再学习

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionServer)

1.3 动作客户端 action client

在动作功能包的/src目录下,编写动作客户端节点,fibonacci_action_client.cpp

#include <functional>
#include <future>   //C++11 异步通信
#include <memory>
#include <string>
#include <sstream>  //C++标准库中 sstream 提供了比ANSI C的 stdio 更高级的一些功能,即单纯性、类型安全和可扩展性。

#include "action_tutorials_interfaces/action/fibonacci.hpp"

#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "rclcpp_components/register_node_macro.hpp"

namespace action_tutorials_cpp
{
class FibonacciActionClient : public rclcpp::Node
{
public:
    using Fibonacci = action_tutorials_interfaces::action::Fibonacci;
    using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>;

    //构造函数:
    //1、初始化动作客户端节点名 fibonacci_action_client
    //2、初始化动作客户端:动作类型Fibonacci、所属节点this、动作名fibonacci
    //3、初始化定时器,每500ms发送一次目标
    explicit FibonacciActionClient(const rclcpp::NodeOptions & options) : Node("fibonacci_action_client", options)
    {
        this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(
        this,
        "fibonacci");

        this->timer_ = this->create_wall_timer(std::chrono::milliseconds(500), std::bind(&FibonacciActionClient::send_goal, this));
    }

    //定时器中断函数
    void send_goal()
    {
        using namespace std::placeholders;

        //取消了定时,也就是说定时器中断函数只执行一次
        this->timer_->cancel();

        //然后,看看有没有在线的同名动作服务器
        if (!this->client_ptr_->wait_for_action_server()) 
        {
            RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting");
            rclcpp::shutdown();
        }

        //定义目标
        auto goal_msg = Fibonacci::Goal();
        goal_msg.order = 10;

        RCLCPP_INFO(this->get_logger(), "Sending goal");

        //确定回调函数:请求后的响应、反馈的处理、结果的接收
        auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions();

        send_goal_options.goal_response_callback =
        std::bind(&FibonacciActionClient::goal_response_callback, this, _1);

        send_goal_options.feedback_callback =
        std::bind(&FibonacciActionClient::feedback_callback, this, _1, _2);

        send_goal_options.result_callback =
        std::bind(&FibonacciActionClient::result_callback, this, _1);

        //发送目标给动作服务器
        this->client_ptr_->async_send_goal(goal_msg, send_goal_options);
    }

private:
    rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_;
    rclcpp::TimerBase::SharedPtr timer_;

    //请求后的响应
    void goal_response_callback(std::shared_future<GoalHandleFibonacci::SharedPtr> future)
    {
        auto goal_handle = future.get();
        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");
        }
    }

    //反馈的处理
    void feedback_callback(GoalHandleFibonacci::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback)
    {
        std::stringstream ss;
        ss << "Next number in sequence received: ";
        for (auto number : feedback->partial_sequence) 
        {
            ss << number << " ";
        }
        RCLCPP_INFO(this->get_logger(), ss.str().c_str());
    }

    //结果的接收
    void result_callback(const GoalHandleFibonacci::WrappedResult & result)
    {
        switch (result.code) 
        {
            case rclcpp_action::ResultCode::SUCCEEDED:  break;
            case rclcpp_action::ResultCode::ABORTED:    RCLCPP_ERROR(this->get_logger(), "Goal was aborted");   return;
            case rclcpp_action::ResultCode::CANCELED:   RCLCPP_ERROR(this->get_logger(), "Goal was canceled");  return;
            default:                    RCLCPP_ERROR(this->get_logger(), "Unknown result code");    return;
        }
        std::stringstream ss;
        ss << "Result received: ";
        for (auto number : result.result->sequence) 
        {
            ss << number << " ";
        }
        RCLCPP_INFO(this->get_logger(), ss.str().c_str());
        rclcpp::shutdown();
    }
};  // class FibonacciActionClient

}  // namespace action_tutorials_cpp

RCLCPP_COMPONENTS_REGISTER_NODE(action_tutorials_cpp::FibonacciActionClient)

注 std::future :看这里。std::thread是C++11中提供异步创建多线程的工具,但想要从线程中返回异步任务结果,一般需要依靠全局变量,从安全角度看,有些不妥,为此C++11提供了std::future类模板,future对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。

1.4 修改CMakelists.txt

在CMakelists.txt添加以下内容。

# 这里是生成共享库
add_library(action_server SHARED src/fibonacci_action_server.cpp)
add_library(action_client SHARED src/fibonacci_action_client.cpp)

target_include_directories(action_server PRIVATE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_include_directories(action_client PRIVATE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)

target_compile_definitions(action_server PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")
target_compile_definitions(action_client PRIVATE "ACTION_TUTORIALS_CPP_BUILDING_DLL")

ament_target_dependencies(action_server
  "action_tutorials_interfaces"
  "rclcpp"
  "rclcpp_action"
  "rclcpp_components")
ament_target_dependencies(action_client
  "action_tutorials_interfaces"
  "rclcpp"
  "rclcpp_action"
  "rclcpp_components")

rclcpp_components_register_node(action_server PLUGIN "action_tutorials_cpp::FibonacciActionServer" EXECUTABLE fibonacci_action_server)
rclcpp_components_register_node(action_client PLUGIN "action_tutorials_cpp::FibonacciActionClient" EXECUTABLE fibonacci_action_client)

install(TARGETS
  action_server
  action_client
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin)

ROS2_Foxy学习5——基础编程_C++

里面的例子参考官方教程,然后附带一些解释和一些推荐的便于理解的文章。

1 workspace

1.1 工作空间及其层次

  空间的创建:ROS2与ROS1一样,创建工作空间目录,并在其中创建/src目录,用于存放package功能包。

  层次的概念:ROS2增加了overlay和underlay的概念,用于在多个workspace同时工作时,处理各个workspace之间的层次问题,对于包名相同的package,上层workspace将覆盖(override)下层workspace中的同名package。

  层次的配置:层次是通过环境变量配置的先后决定的,ROS通过setup.bash设置环境变量,ROS2安装路径一般设置为最下层的工作空间,即

$ source /opt/ros/foxy/setup.bash

  运行新建workspace的package功能包,需要在对工作空间编译后的新终端中配置如下

# 设置当前工作空间中功能包的相关环境变量
$ . install/local_setup.sh 

# 设置当前工作空间中功能包的相关环境变量,同时设置该工作空间下其他底层工作空间的环境变量
$ . install/setup.sh    # 就是说这一个捎带着把ROS2安装路径的环境变量也设置了

1.2 检查依赖

  编译前,需要对功能包检查依赖情况。

$ rosdep install -i --from-path src --rosdistro foxy -y

1.3 编译工具 colcon

# 在workspace根目录编译工程
$ colcon build  

# 有选择地编译包
$ colcon build --packages-select <package_name>

  有关编译,可看一下后续的系列第七篇——编译

2 编写 package

  每一个package都能实现一个相对完整的功能。

2.1 功能包创建

# 创建cmake功能包(在~/ws/src下)
$ ros2 pkg create --build-type ament_cmake <package_name>

# 创建cmake功能包(在~/ws/src下)
# --dependencies 参数会将后边的依赖自动添加到package.xml和CMakeLists.txt中
$ ros2 pkg create --build-type ament_cmake <package_name> --dependencies <depend_name>

# 创建cmake功能包(在~/ws/src下)
# --node-name 参数将创建节点,但只能创建一个节点,注意参数位置
$ ros2 pkg create --build-type ament_cmake --node-name <node_name> <package_name>

    # 例如(在~/ws/src下)
    $ ros2 pkg create --build-type ament_cmake --node-name my_node my_package --dependencies rclcpp
    # 将会在~/ws/src创建如下目录
    workspace_folder/
        src/ 
            my_package/ 
                 src/
                    my_node.cpp
                 include/
                    my_package/
                 CMakeLists.txt
                 package.xml

2.2 package.xml 和 CMakeLists.txt

  使用ament编译系统的功能包会对应生成两个文件package.xml和CMakeLists.txt。
  package.xml:描述了功能包的发行信息、依赖信息等,内容如下。在自动创建后,需要手动修改功能包描述、功能包版本、功能包许可证信息等发行信息。以“ _depend”结尾的标签用来描述功能包依赖,在增加新的依赖时,需要添加到其中。

<!--  功能包基础内容,不用修改   -->
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<!--  下面是功能包的信息  -->
<package format="3">
    <!--  包名,新建的时候就定了,不改  -->
    <name>cpp_srvcli</name>
    <!--  关于发行信息   -->
    <version>0.0.0</version>
    <description>这里写功能描述</description>
    <maintainer email="这里写邮箱地址">Your Name</maintainer>
    <license>这里写许可证</license>
    <!--  编译系统的依赖   -->
    <buildtool_depend>ament_cmake</buildtool_depend>
    <!--  程序所需的依赖,根据需要修改添加   -->
    <depend>rclcpp</depend>
    <!--  编译系统,新建功能包的时候可以指定   -->
    <export>
    <build_type>ament_cmake</build_type>
    </export>
</package>

  CMakeLists.txt:制定各文件或包的编译顺序的文档,如何编写CMakeLists.txt参见本系列第七篇——编译。文档基本结构如下。

cmake_minimum_required(VERSION 3.5) //cmake版本要求
project(my_project)                 //功能包名,与同一功能包下package.xml中名字一样

ament_package()                     //配置project,只调用一次,且在最后

2.3 功能包编译、设置环境变量、运行

# 记得检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y

# 编译,见上一小节
$ colcon build

# 新终端设置环境变量
$ . install/setup.bash

# 运行包里的某个节点,运行多个节点、多个包,可使用launch文件
$ ros2 run <package_name> <node_name>

3 编写 topic

新建工作空间,并新建话题功能包(这里包名是cpp_pubsub)。

3.1 发布 publisher

在功能包的/src目录下,编写话题发布节点,publisher_member_function.cpp

#include <chrono>           //c++11日期和时间库       
#include <functional>       //参见 注1
#include <memory>           //c++内存管理库,使用智能指针必须包含此头文件
#include <string>

#include "rclcpp/rclcpp.hpp"    //ROS2,C++接口的头文件
#include "std_msgs/msg/string.hpp"  //string类型的msg头文件

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. */

//定义一个节点类的子类
class MinimalPublisher : public rclcpp::Node
{
  public:
        //构造函数:初始化节点名 minimal_publisher,然后创建了一个publisher和定时器
        MinimalPublisher(): Node("minimal_publisher"), count_(0)
        {
            //创建一个发布者,发布的话题名为topic,话题消息是String,保存消息的队列长度是10
            publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
            //创建一个定时器,定时500ms,定时触发回调函数timer_callback。
            //这里用到了<functional>的一个特性
            timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::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<MinimalPublisher>());
        rclcpp::shutdown();
        return 0;
  }

注:
1、#include < functional > ,感兴趣的读者可以看下这篇博客,写的蛮不错的。

3.2 订阅 subscriber

在功能包的/src目录下,编写话题订阅节点,subscriber_member_function.cpp

#include <memory>

#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;
}

3.3 修改 package.xml / CMakeLists.txt

  package.xml 添加如下依赖,如果创建包时,使用了–dependencies,则文件内已有如下语句。

    <depend>rclcpp</depend>
    <depend>std_msgs</depend>

  CMakeLists.txt 添加如下编译规则,如果创建包时,使用了–dependencies,则文件内已有find_package相关的语句。

# 搜索依赖
    find_package(rclcpp REQUIRED)
    find_package(std_msgs REQUIRED)
# 创建可执行程序talker,后续参数是构成可执行文件的所有源文件
    add_executable(talker src/publisher_member_function.cpp)
# 链接find_package()找到的包
    ament_target_dependencies(talker rclcpp std_msgs)

    add_executable(listener src/subscriber_member_function.cpp)
    ament_target_dependencies(listener rclcpp std_msgs)

    install(TARGETS
      talker
      listener
      DESTINATION
      lib/${PROJECT_NAME})

3.4 编译、运行两个节点

# 在工作空间根目录下,检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y
# 编译
$ colcon build

# 配置环境变量,然后运行
$ . install/setup.bash
$ ros2 run cpp_pubsub talker

# 打开另一个终端,配置环境变量,运行
$ . install/setup.bash
$ ros2 run cpp_pubsub listener

4 编写 service

新建工作空间,并新建服务功能包(这里包名是cpp_srvcli)。

4.1 服务器端 server

在功能包的/src目录下,创建服务器端节点,add_two_ints_server.cpp

#include "example_interfaces/srv/add_two_ints.hpp"
#include "rclcpp/rclcpp.hpp"
#include <memory>

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)
{
    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.");

    //入锁,等待客户端请求
    rclcpp::spin(node);
    rclcpp::shutdown();
}

4.2 客户端 client

在功能包的/src目录下,创建客户端节点,add_two_ints_client.cpp

#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;
    }

    //创建名为add_two_ints_client的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");

    //创建名为add_two_ints的客户端
    rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
    node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

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

    //search for service
    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...");
    }

    //发送request
    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;
}

4.3 修改 package.xml / CMakeLists.txt

  package.xml 添加如下依赖,如果创建包时,使用了–dependencies,则文件内已有如下语句。

    <depend>rclcpp</depend>
    <depend>example_interfaces</depend>

  CMakeLists.txt 添加如下编译规则,如果创建包时,使用了–dependencies,则文件内已有find_package相关的语句。

# 搜索依赖
    find_package(rclcpp REQUIRED)
    find_package(example_interfaces REQUIRED)
# 创建可执行程序server,后续参数是构成可执行文件的所有源文件
    add_executable(server src/add_two_ints_server.cpp)
# 链接find_package()找到的包
    ament_target_dependencies(server rclcpp example_interfaces)

    add_executable(client src/add_two_ints_client.cpp)
    ament_target_dependencies(client rclcpp example_interfaces)

    install(TARGETS
              server
              client
              DESTINATION lib/${PROJECT_NAME})

4.4 编译、运行两个节点

# 在工作空间根目录下,检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y
# 编译
$ colcon build

# 配置环境变量,然后运行
$ . install/setup.bash
$ ros2 run cpp_srvcli server

# 打开另一个终端,配置环境变量,运行
$ . install/setup.bash
$ ros2 run cpp_srvcli client 2 3

5 自定义消息 msg/srv

5.1 创建消息功能包

1、新建工作空间,并新建消息功能包(这里包名是tutorial_interfaces)。在消息功能包下,创建/msg和/srv文件夹(与/src同级),并在其中分别建立.msg和.srv文件,有关消息内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

Num.msg

int64 num

AddThreeInts.srv

int64 a
int64 b
int64 c
---
int64 sum

2、在package.xml文件中添加

    <build_depend>rosidl_default_generators</build_depend>
    <exec_depend>rosidl_default_runtime</exec_depend>
    <member_of_group>rosidl_interface_packages</member_of_group>

注:rosidl_default_generators为编译依赖,rosidl_default_runtime为运行依赖。
3、在CMakeLists.txt文件中添加

find_package(rosidl_default_generators REQUIRED)

# You can use set to neatly list all of your interfaces:
set(msg_files
  "msg/Num.msg"
  "msg/Message1.msg"
  "msg/Message2.msg"
  # etc
  )

set(srv_files
  "srv/AddThreeInts.srv"
  "srv/Service1.srv"
  "srv/Service2.srv"
   # etc
  )

#And generate all lists at once like so:
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
  ${srv_files}
)

ament_export_dependencies(rosidl_default_runtime)

注:可以省略set的步骤,在rosidl_generate_interfaces直接添加,如下,但不建议,还是需要一个良好的编程习惯。

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "srv/AddThreeInts.srv"
)

5.2 测试自定义消息接口

  功能包编译后(没有节点,没有运行),可以通过命令ros2 interface show来查看消息文件。

5.3 应用自定义消息接口

  在同一个工作空间下,除消息功能包外,再创建两个功能包,分别是话题功能包和服务功能包,参考3、4部分。

1、CMakeLists.txt和package.xml与第3、4部分基本一致,其中,需要将消息依赖包(std_msgs和example_interfaces)替换成新建的消息功能包名,如tutorial_interfaces。
2、各节点src文件,需要将消息类型进行替换,src文件修改见附件1。
3、之后,编译运行(查依赖、编译包、配置环境、运行节点)。

5.4 使用同一功能包的消息接口

  若功能包需要使用自己定义的接口,需要在CMakeLists.txt中添加以下内容(In order to use the messages generated in the same package.)。

rosidl_target_interfaces(publish_address_book
  ${PROJECT_NAME} "rosidl_typesupport_cpp")

注:publish_address_book是编译生成的可执行程序名。

6 使用 parameter

新建工作空间,并新建参数功能包(这里包名是cpp_parameters),使用参数dependencies,自动添加依赖rclcpp。

6.1 参数的声明与获取

在功能包的/src目录下,创建节点,cpp_parameters_node.cpp

#include <rclcpp/rclcpp.hpp>
#include <chrono>
#include <string>
#include <functional>

using namespace std::chrono_literals;

class ParametersClass: public rclcpp::Node
{
  public:
    ParametersClass() : Node("parameter_node")
    {
      //声明:定义参数名及其值
      this->declare_parameter<std::string>("my_parameter", "world");
      //
      timer_ = this->create_wall_timer(1000ms, std::bind(&ParametersClass::respond, this));
    }

    //获取:定时打印参数值
    void respond()
    {
      this->get_parameter("my_parameter", parameter_string_);
      RCLCPP_INFO(this->get_logger(), "Hello %s", parameter_string_.c_str());
    }

  private:
    std::string parameter_string_;
    rclcpp::TimerBase::SharedPtr timer_;
};

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

修改CMakelists.txt,然后编译运行。

add_executable(parameter_node src/cpp_parameters_node.cpp)
ament_target_dependencies(parameter_node rclcpp)

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

6.2 终端修改参数

有关内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

$ ros2 param list
$ ros2 param set <node_name> <parameter_name> <value>

6.3 launch文件修改参数

在功能包的/launch目录下,创建launch文件,cpp_parameters_launch.py,修改parameters。

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package="cpp_parameters",
            executable="parameter_node",
            name="custom_parameter_node",
            output="screen",
            emulate_tty=True,
            parameters=[
                {"my_parameter": "earth"}
            ]
        )
    ])

修改CMakelists.txt,在6.1部分中CMakelists.txt最后增加以下部分。

install(
  DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)

编译,然后运行launch文件。

$ ros2 launch cpp_parameters cpp_parameters_launch.py

附件1 消息文件应用

1、话题功能包——消息发布节点——publisher_member_function.cpp

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"

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. */

class MinimalPublisher : public rclcpp::Node
{
public:
    MinimalPublisher(): Node("minimal_publisher"), count_(0)
    {
      publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); //10 是消息队列长度
      timer_ = this->create_wall_timer(500ms,std::bind(&MinimalPublisher::timer_callback, this));
    }

private:
    void timer_callback()
    {
      auto message = tutorial_interfaces::msg::Num();
      message.num = this->count_++;
      RCLCPP_INFO(this->get_logger(), "Publishing: '%d'", message.num);
      publisher_->publish(message);
    }

    rclcpp::TimerBase::SharedPtr timer_;

    rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_;

    size_t count_;
};

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

2、话题功能包——消息订阅节点——subscriber_member_function.cpp

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
    MinimalSubscriber(): Node("minimal_subscriber")
    {
      subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>("topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

private:
    void topic_callback(const tutorial_interfaces::msg::Num::SharedPtr msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%d'", msg->num);
    }
    rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_;};

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

3、服务功能包——服务器端节点——add_three_ints_server.cpp

#include "tutorial_interfaces/srv/add_three_ints.hpp"
#include "rclcpp/rclcpp.hpp"
#include <memory>

void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,
      std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response>      response)
{
    response->sum = request->a + request->b + request->c;
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld",
            request->a, request->b, request->b);
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

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

    //创建名为add_two_ints_server的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server");

    //创建名为add_two_ints的服务,绑定回调函数add
    rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =
    node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add);

    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints.");

    //入锁,等待客户端请求
    rclcpp::spin(node);
    rclcpp::shutdown();
}

4、服务功能包——客户端节点——add_three_ints_client.cpp

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_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 != 4) 
    {
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y Z");
        return 1;
    }

    //创建名为add_three_ints_client的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");

    //创建名为add_three_ints的客户端
    rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =
    node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");

    //采集request数据
    auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();
    request->a = atoll(argv[1]);
    request->b = atoll(argv[2]);
    request->c = atoll(argv[3]);

    //search for service
    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...");
    }

    //发送request
    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_three_ints");
    }

    rclcpp::shutdown();
    return 0;
}

ROS2_Foxy学习4——常用工具

1 rqt

1.1 安装

  不同的版本 安装方式不同。

# apt2.0 Ubuntu 20.04 and newer
    $ sudo apt update
    $ sudo apt install ~nros-<distro>-rqt*
# apt1.x Ubuntu 18.04 and older
    $ sudo apt update
    $ sudo apt install ros-<distro>-rqt*

1.2 rqt_graph

  运行rqt_graph,也可以从rqt打开,rqt_graph清晰地展示了节点、话题等图元素的关系。
在这里插入图片描述

1.3 rqt_console

  记录终端的日志,也是一个节点,收集/rosout的信息,可以根据日志等级过滤,也可以高亮一些内容。

$ ros2 run rqt_console rqt_console

在这里插入图片描述
  上中下一共三个窗口,上面窗口列出了所有的日志,中间窗口可以根据日志等级进行筛选,下面的额窗口可以高亮一些需要的字符串。
  日志等级按严重性,如下排列:

Fatal
Error
Warn
Info
Debug

2 launch file

  Launch files allow you to start up and configure a number of executables containing ROS 2 nodes simultaneously.

$ ros2 launch <launch_file_name>

  launch文件的编写在后续博文中单列一节~

3 ros2bag

  记录topic发布的数据,可选择记录哪些topic,事后可以对ros包进行回放。

# 录包
    # 包存在于 录包终端 所在的目录下
    $ ros2 bag record <topic_name>

    # 同时录多个话题,话题之间使用空格分开
    # 使用 -o 选项可以设置包名
    $ ros2 bag record -o <bag_file_name> <topic1_name> <topic2_name>
    # 使用 -a 选项是记录所有topic,在存在循环依赖的系统中 不建议使用
    $ ros2 bag record -a

# 查看包的信息
    $ ros2 bag info <bag_file_name>
    # 有这些信息
    Files:             subset.db3
    Bag size:          228.5 KiB
    Storage id:        sqlite3
    Duration:          48.47s
    Start:             Oct 11 2019 06:09:09.12 (1570799349.12)
    End                Oct 11 2019 06:09:57.60 (1570799397.60)
    Messages:          3013
    Topic information: Topic: /turtle1/cmd_vel | Type: geometry_msgs/msg/Twist | Count: 9 | Serialization Format: cdr
                     Topic: /turtle1/pose | Type: turtlesim/msg/Pose | Count: 3004 | Serialization Format: cdr

# 回放
    $ ros2 bag play <bag_file_name>

ROS2_Foxy学习3——核心概念

  A series of core ROS 2 concepts that make up what is referred to as the “ROS (2) graph”.

  这部分将介绍组成ROS2图的各个核心概念,包括节点、话题、服务、参数、动作。Graph展示了机器人系统内部各部分配合运行的过程。

1 node 节点

  一个完整的机器人系统,或者图,由许多的节点组成。每个节点都负责一个模块的功能。节点之间互相配合使得系统实现某些更为高级的功能。
  例如,一个基于ROS的轮式移动机器人,节点 /sensor 负责采集周围环境信息,节点 /controler 负责判断障碍物信息,节点 /motion 负责改变车轮的转速和转向,三者配合实现避障的功能,其中三者之间的数据交互可以通过topic、service等实现。

# 节点启动
    $ ros2 run <package_name> <executable_name>
    $ ros2 run turtlesim turtlesim_node
# 列出所有节点
    $ ros2 node list
# 节点信息:查看包括订阅和发布的话题、服务、动作等
    $ ros2 node info <node_name>

2 parameter 参数

  每个节点有自己的参数/配置,参数可以动态调整。

# 列出每个节点的参数
    $ ros2 param list

# 获取某个参数的类型和值
    $ ros2 param get <node_name> <parameter_name>
    # 例如
        $ ros2 param get /turtlesim background_g
    # return
        Integer value is: 86

# 设置某个参数的值
    $ ros2 param set <node_name> <parameter_name> <value>
    # 例如
        $ ros2 param set /turtlesim background_r 150
    # return
        Set parameter successful

# 保存参数到yaml文件
    $ ros2 param dump <node_name>
    # 例如
        $ ros2 param dump /turtlesim
    # return
        Saving to:  ./turtlesim.yaml

# 加载保存的yaml文件
    $ ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>
    # 例如
        $ ros2 run turtlesim turtlesim_node --ros-args --params-file ./turtlesim.yaml

3 topic 话题

在这里插入图片描述

3.1 topic 话题

  topic是节点之间数据交互的一种方式,发送数据的节点采用publish的方式将数据发布到topic,而接收数据的节点采用subscribe的方式从topic订阅数据,可以看下这个官方的gif

# 列出所有话题
    $ ros2 topic list
    $ ros2 topic list -t    # 带topic类型

# 查看话题数据
    $ ros2 topic echo <topic_name>

# 查看话题的类型、发布者和订阅者数量
    $ ros2 topic info <topic_name>
    # 例如
        $ ros2 topic info /turtle1/cmd_vel
    # return
        Type: geometry_msgs/msg/Twist
        Publisher count: 1
        Subscription count: 1

# 查看数据发布频率
    $ ros2 topic hz /turtle1/pose

  These attributes, particularly the type, are how nodes know they’re talking about the same information as it moves over topics.

  roscli相关的节点是命令行程序创建的。

3.2 topic 消息类型 msg

  Nodes send data over topics using messages. Publishers and subscribers must send and receive the same type of message to communicate.

# 查看topic类型
    $ ros2 interface show <type>.msg
    # 例如
        $ ros2 interface show geometry_msgs/msg/Twist
    # return
        # This expresses velocity in free space broken into its linear and angular parts.

        Vector3  linear
        Vector3  angular

3.3 终端发布数据到话题 pub

$ ros2 topic pub <topic_name> <msg_type> '<args>'
    # 例如
    $ 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 代表 只发送一次数据即退出
    # --rate 1 代表 以1Hz的频率发送数据

4 service 服务

在这里插入图片描述

4.1 service 服务

  节点之间数据交互的另一种方式是service。话题通过发布与订阅进行(publisher-subscriber model),服务通过请求与应答进行(call-and-response model),只有请求才会应答。
  service包括客户端(service client)和服务器(service server),对于同一个service可以有多个client,但只能有一个server。

# 列出所有服务
    $ ros2 service list
    $ ros2 service list -t  # 带service类型

4.2 service 消息类型 srv

  service的消息不同于topic的消息,前者的消息包括两部分请求消息和应答消息。

# 查看service消息类型
    $ ros2 service type <service_name>
    # 例如
        $ ros2 service type /clear
    # return,Empty 表示 请求和应答都没有数据
        std_srvs/srv/Empty

# 查看service消息类型结构
    $ ros2 interface show <type_name>.srv
    # 例如
        $ ros2 interface show turtlesim/srv/Spawn.srv
    # return,--- 将请求数据和应答数据分开
        float32 x
        float32 y
        float32 theta
        string name # Optional.  A unique name will be created and returned if this is empty
        ---
        string name

# 根据service消息类型找服务
    $ ros2 service find <type_name>

4.3 终端请求服务 call

$ ros2 service call <service_name> <service_type> <arguments>
    # 例如
        $ ros2 service call /clear std_srvs/srv/Empty
    # return
        waiting for service to become available...
        requester: making request: std_srvs.srv.Empty_Request()

        response:
        std_srvs.srv.Empty_Response()

    # 例如
        $ ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: ''}"
    # return
        waiting for service to become available...
        requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.2, name='')

        response:
        turtlesim.srv.Spawn_Response(name='turtle2')

5 action 动作

在这里插入图片描述

5.1 action 动作

  action是节点间长时间通信的的一种方式,基于topic和service,组成如上图
  首先,action client发出目标服务请求,action server告知action client收到目标请求;然后,action client发出结果服务请求,action server返回反馈流和结果。
  action是可抢占的,也就是可以中途停止,或更换目标。

# 列出所有的action
    $ ros2 action list
    $ ros2 action list -t # 带action类型

# 查看action信息
    $ ros2 action info <action_name>
    # 例如
        $ ros2 action info /turtle1/rotate_absolute
    # return
        Action: /turtle1/rotate_absolute
        Action clients: 1
            /teleop_turtle
        Action servers: 1
            /turtlesim

5.2 action 消息类型 action

# 查看action类型
    $ ros2 interface show <type_name>.action
    # 例如
        $ ros2 interface show turtlesim/action/RotateAbsolute.action
    # return
        # 请求
        float32 theta
        ---
        # 结果
        float32 delta
        ---
        # 反馈
        float32 remaining

5.3 终端发送动作请求 send_goal

    $ ros2 action send_goal <action_name> <action_type> <values>
    # 例如
        $ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}"
    # return
        Waiting for an action server to become available...
        Sending goal:
           theta: 1.57

        Goal accepted with ID: f8db8f44410849eaa93d3feb747dd444

        Result:
          delta: -1.568000316619873

        Goal finished with status: SUCCEEDED

    # 带反馈 --feedback
    $ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback

  每个动作目标都有一个对应ID。

6 rqt_graph

  运行rqt_graph,也可以从rqt打开。
在这里插入图片描述

7 小结

  节点是一个完整机器人系统的基础组成部分,每个节点有自己的参数,这些参数可以动态配置。节点间的通信方式包括topic(publisher-subscriber model)、service(call-and-response model)和action(topic-service model),topic适合于描述实时状态的数据交流,service适合于固定数据请求或指令的发布,action适合于发布指令(需要花费一段时间才能完成的指令,for long running tasks)、获取反馈等情形。
  rqt_graph提供了一个可视化界面,能够清晰地描述系统工作过程中各部分的依赖关系。

ROS2_Foxy学习2——环境配置

1 配置环境变量

  使用ROS前需要配置ROS2的环境变量,方式是通过ROS2安装文件的setup.bash:对于二进制文件安装和编译安装,bash文件所在位置不同。

# 二进制文件安装
$ source /opt/ros/foxy/setup.bash
# 编译安装,在install文件夹内
$ source ~/ros2_foxy/install/setup.bash

注意:编译后build文件夹不可删除,~/ros2_foxy/install/setup.bash关联/build内的bash文件。

  每次打开新的终端都需要source一次,以获取ROS2安装文件,也可以选择将source的过程添加到启动脚本中,这样就不需要每次都source。

$ echo "source /opt/ros/foxy/setup.bash" >> ~/.bashrc

  另外,启动脚本的修改只需要打开bashrc文件修改即可

$ sudo gedit ~/.bashrc

注意:启动脚本修改后,需要重启终端以实现。

2 检查环境变量

  如果在编译时找不到包,可以检查环境变量,foxy应该有以下内容。

$ printenv | grep -i ROS

ROS_VERSION=2
ROS_PYTHON_VERSION=3
ROS_DISTRO=foxy

  对于多机问题,为区分各组机器人,需要修改一个环境变量ROS_DOMAIN_ID,其值是0~232的整数。ROS_DOMAIN_ID相同的机器人属于一个组,可以通信,而不同域ID的机器人不能通信且互不影响。环境变量的设置如下:

# 注意 !!! 等号两侧不要有空格
$ export ROS_DOMAIN_ID=<your_domain_id> 
# 也可以写入启动脚本
$ echo "export ROS_DOMAIN_ID=<your_domain_id>" >> ~/.bashrc

ROS2_Foxy学习1——前言与安装

前言

1、关于ROS2.0的发布背景、优势等,可以参考古月居的ROS2探索总结系列

2、相比于ROS1,ROS2的特点主要有以下几点:

(1)跨平台
  支持构建的系统包括Linux、windows、Mac、RTOS等。

(2)DDS
  ROS2取消了master机制,取而代之的是DDS(Data Distribution Service,数据分发服务)。DDS 的技术核心是以数据为核心的发布订阅模型(Data-Centric Publish-Subscribe ,DCPS),这种DCPS模型创建了一个“全局数据空间”(global data space)的概念,所有独立的应用都可以去访问。在DDS中,每一个发布者或者订阅者都成为参与者(participant),类似于ROS中节点的概念。每一个参与者都可以使用某种定义好的数据类型来读写全局数据空间。
  下图分别是ROS1 架构和ROS2 架构:
ROS1 架构

ROS2 架构
(3)对多机系统更为友好

3、学习过程中,主要使用两种运行环境:Ubuntu 20.04 amd64Ubuntu 20.04 mate arm64,其中前者运行在笔记本上,后者运行在树莓派4B上。以amd64为主,对于arm64的差异会有一定的补充说明。

4、在学习过程中,主要参考古月居的文章和ROS2的官方教程。官方教程除了在线阅读之外,还可以下载到本地

$ git clone https://github.com/ros2/ros2_documentation.git
$ cd ~/ros2_documentation   #注意,这个路径可能不一样
$ make html

可能需要安装下面两个Python包

$ pip3 install sphinx
$ pip3 install sphinx_tabs

5、前言就这些,也许还会补充…

安装

  在Ubuntu 20.04 amd64上安装ROS2:根据官方教程Installing ROS 2 on Linux即可。
  在Ubuntu 20.04 mate arm64上装ROS2:根据官方教程Building ROS 2 on Linux即可。

  由于ROS2对arm架构的树莓派4B的支持不够,所以选择编译源码的方式安装,过程如下。

安装环境:基于树莓派4B与Ubuntu20.04 mate

硬件:树莓派4B arm64
系统:Ubuntu 20.04 mate
ROS:ROS Foxy

编译过程非常漫长,不算解决问题的时间,大概需要三到四个小时…

注意 编译后build文件夹不可删除,~/ros2_foxy/install/setup.bash关联build内的bash文件。

所遇问题

1、步骤 Add the ROS 2 apt repository

$ gpg:no valid openPGP data found

解决方案参考,这个问题在amd64安装中也存在:

# 打开hosts文件
$ sudo gedit /etc/hosts
# 在文件末尾添加
151.101.84.133 raw.githubusercontent.com

2、步骤 Install dependencies using rosdep

解决方案同上

ERROR: cannot download default source list from:
https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/sources.list.d/20-default.list
website may be down.

3、步骤 Build the code in the workspace——mimick_vendor

[Processing: mimick_vendor, orocos_kdl, rviz_ogre_vendor, urdfdom]                  
--- stderr: mimick_vendor                                                           
CMake Error at CMakeLists.txt:88 (message):
  Architecture 'armv7l' is not supported.

make[2]: *** [CMakeFiles/mimick-ext.dir/build.make:106: mimick-ext-prefix/src/mimick-ext-stamp/mimick-ext-configure] Error 1
make[1]: *** [CMakeFiles/Makefile2:137: CMakeFiles/mimick-ext.dir/all] Error 2
make: *** [Makefile:141: all] Error 2
---
Failed   <<< mimick_vendor [57.1s, exited with code 2]

原因:

(1)mimick不支持armv7l架构。
(2)GitHub有两个mimick,分别是Snaipe/Mimickros2/Mimick,后者在前者的基础上修改应用到ros2。
(3)根据这里,arm7l是一个不同于arm32的体系结构,如果没有明确表示为单独的体系结构选项,它将不会被检测为兼容的体系结构。
(4)解决方案参考Snaipe/Mimick#17:在Snaipe/Mimickros2/Mimick的CMakeLists.txt中,有如下部分,可以看到不支持armv7l架构。

if (MSVC)
  //省略
else ()
  set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-parameter")

  enable_language (ASM)
  if (_ARCH MATCHES "${I386}")
    //省略
  elseif (_ARCH MATCHES "${AMD64}")
    //省略
  elseif (_ARCH MATCHES "${ARM32}")
    //省略
  elseif (_ARCH MATCHES "${ARM64}")
    //省略
  //加在这里***  
  else ()
    message (FATAL_ERROR "Architecture '${_ARCH}' is not supported.")
  endif ()
  set (ASM_EXTENSION ".S")
endif ()

解决方法是在对应位置增加以下代码段:

elseif (_ARCH MATCHES "armv7l")
    set (MMK_ARCH "arm")
    set (MMK_ABI "arm")
    set (MMK_BITS 32)
    set (MMK_ARCH_ARM 1)

(5)但是,这个CMakeLists.txt并不在/src/ros2/mimick_verdor文件夹内,而是在 /build/mimick_vendor/mimick-ext-prefix/src/mimick-ext,/src内只提供了url。需要打补丁的CMakeLists.txt可以在编译出错后找到,修改,再编译即可。
(6)这个问题估计很快就会被官方修改,期待…

4、步骤 Build the code in the workspace

如果觉得编译时间太长,可以选择不编译一些包,如一些demo,方法是在对应目录中,添加一个文件

$ touch AMENT_IGNORE

5、步骤 Build the code in the workspace——tracetools

--- stderr: rcl                                                                                                                           
    CMake Error at CMakeLists.txt:13 (find_package):
      By not providing "Findtracetools.cmake" in CMAKE_MODULE_PATH this project
      has asked CMake to find a package configuration file provided by
      "tracetools", but CMake did not find one.

      Could not find a package configuration file provided by "tracetools" with
      any of the following names:

        tracetoolsConfig.cmake
        tracetools-config.cmake

      Add the installation prefix of "tracetools" to CMAKE_PREFIX_PATH or set
      "tracetools_DIR" to a directory containing one of the above files.  If
      "tracetools" provides a separate development package or SDK, be sure it has
      been installed.
    ---

检查发现目录/src/ros-tracing/ros2_tracing下是空的,查看repos,找到如下内容

ros-tracing/ros2_tracing:
    type: git
    url: https://gitlab.com/ros-tracing/ros2_tracing.git
    version: foxy

然后,重新在对应的地址下载源码,解压到对应目录,再编译。

运动规划学习笔记5——ROS包_tf2

在这里分享了运动规划方面的一些基本的算法原理和伪代码实现,其中内容可能存在不完善和错误之处,如有读者发现,欢迎批评指正。

1、坐标系

  1. 一个机器人拥有多个坐标系,这些坐标系在tf2中构成坐标树,tf2通过坐标树维护多个坐标系之间的坐标变换。
  2. ROS坐标系为右手坐标系,前X、左Y、上Z。
  3. 两坐标系之间关系用6自由度相对位姿表示,tf2中由tf2::Vector3tf2::Quaternion表示。
  4. 查看坐标树
    # 方法一:view_frames工具
    rosrun tf view_frames 
    # 方法二:rqt
    rosrun rqt_tf_tree rqt_tf_tree
  5. 查看坐标转换关系
    # 变换关系
    rosrun tf tf_echo /world /turtle1
  6. 查看坐标转换时间延迟
    # 时间延迟
    rosrun tf tf_monitor /world /turtle1

2、坐标变换

2.1、坐标变换

  1. 坐标变换库:tf2_ros,http://docs.ros.org/en/jade/api/tf2_ros/html/c++/namespacetf2__ros.html
  2. 坐标转换消息类型:geometry_msgs/TransformStamped,包括父坐标系、子坐标系、转换关系
    std_msgs/Header header
    uint32 seq
    time stamp
    string frame_id
    string child_frame_id
    geometry_msgs/Transform transform
    geometry_msgs/Vector3 translation
    float64 x
    float64 y
    float64 z
    geometry_msgs/Quaternion rotation
    float64 x
    float64 y
    float64 z
    float64 w

2.2、广播坐标变换

  1. 广播坐标转换的两种情况
    广播静态坐标转换
    广播器:tf2_ros::StaticTransformBroadcaster
    广播方法:tf2_ros::StaticTransformBroadcaster::sendTransform(geometry_msgs/TransformStamped)
    广播动态坐标转换
    广播器:tf2_ros::TransformBroadcaster
    广播方法:tf2_ros::TransformBroadcaster::sendTransform(geometry_msgs/TransformStamped)
    广播方法:
    1、发送一个TransformStamped消息
    void sendTransform (const geometry_msgs::TransformStamped &transform)
    2、发送一组TransformStamped消息
    void sendTransform (const std::vector< geometry_msgs::TransformStamped > &transforms)
  2. 广播静态坐标转换

    static tf2_ros::StaticTransformBroadcaster static_br;
    geometry_msgs::TransformStamped static_tfStamped;
    
    static_tfStamped.header.stamp = ros::Time::now();
    static_tfStamped.header.frame_id = "parent_frame";
    static_tfStamped.child_frame_id = "child_frame";
    static_tfStamped.transform.translation.x = atof(x);
    static_tfStamped.transform.translation.y = atof(y);
    static_tfStamped.transform.translation.z = atof(z);
    
    tf2::Quaternion quat;
    quat.setRPY(atof(wx), atof(wy), atof(wz);
    static_tfStamped.transform.rotation.x = quat.x();
    static_tfStamped.transform.rotation.y = quat.y();
    static_tfStamped.transform.rotation.z = quat.z();
    static_tfStamped.transform.rotation.w = quat.w();
    
    static_br.sendTransform(static_tfStamped);
  3. 广播动态坐标转换

    void callback(const xxx& msg)
    {
     static tf2_ros::TransformBroadcaster br;
     geometry_msgs::TransformStamped tfStamped;
    
     tfStamped.header.stamp = ros::Time::now();
     tfStamped.header.frame_id = "parent_frame";
     tfStamped.child_frame_id = "child_frame";
     tfStamped.transform.translation.x = msg->x;
     tfStamped.transform.translation.y = msg->y;
     tfStamped.transform.translation.z = msg->z;
    
     tf2::Quaternion q;
     q.setRPY(msg->wx, msg->wy, msg->wz);
     tfStamped.transform.rotation.x = q.x();
     tfStamped.transform.rotation.y = q.y();
     tfStamped.transform.rotation.z = q.z();
     tfStamped.transform.rotation.w = q.w();
    
     br.sendTransform(tfStamped);
    }

2.3、监听坐标转换

  1. 监听坐标转换

    监听坐标转换
    监听器:tf2_ros::TransformListener
    监听缓存设置
    缓存器:tf2_ros::Buffer 默认10s缓存
    监听方法
    geometry_msgs::TransformStamped     lookupTransform (const std::string &target_frame, const std::string &source_frame, const ros::Time &time, const ros::Duration timeout) const
    #include <tf2_ros/transform_listener.h>
    
    tf2_ros::Buffer tfBuffer;
    tf2_ros::TransformListener tfListener(tfBuffer);
    
    try
    {
        transformStamped = tfBuffer.lookupTransform("turtle2", "turtle1",ros::Time(0));
    }
    catch (tf2::TransformException &ex) 
    {
        ROS_WARN("%s",ex.what());
        ros::Duration(1.0).sleep();
        continue;
    }

3、问题

/usr/bin/env: “python”: 没有那个文件或目录

  1. 背景
    ubuntu20.04
    roslaunch turtle_tf turtle_tf_demo.launch
  2. 解决
    sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1000

    TypeError: cannot use a string pattern on a bytes-like object

    https://blog.csdn.net/xiaowang_tongxue/article/details/108298544

  3. 问题
    Listening to /tf for 5.0 seconds
    Done Listening
    b'dot - graphviz version 2.43.0 (0)\n'
    Traceback (most recent call last):
    File "/opt/ros/noetic/lib/tf/view_frames", line 119, in <module>
    generate(dot_graph)
    File "/opt/ros/noetic/lib/tf/view_frames", line 89, in generate
    m = r.search(vstr)
    TypeError: cannot use a string pattern on a bytes-like object
  4. 解决
    sudo gedit /opt/ros/noetic/lib/tf/view_frames
    88       print(vstr)
          vstr=str(vstr)  //加入这个...
    89       m = r.search(vstr)