ROS知识积累

本文最后更新于 2024年10月16日 晚上

本文主要积累了平时使用到的ROS知识。

代码编译和运行

示范

  1. 新建工作区

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cd youfolder/
    mkdir -p catkin_ws/src # catkin_ws可改名

    cd catkin_ws
    catkin config --init --mkdirs --extend /opt/ros/melodic --merge-devel --cmake-args -DCMAKE_BUILD_TYPE=Release

    cd src
    catkin_init_workspace # 创建顶层CMakeLists.txt文件,CLion利用此文件打开项目

    cd ..
    catkin build # 编译生成完整工作区

    此时,./catkin_ws文件夹下有builddevellogssrc四个文件夹。

  2. 编译代码

    1
    2
    3
    4
    5
    6
    7
    8
    cd src
    # 把代码总放在src文件夹下
    git clone https://github.com/HKUST-Aerial-Robotics/VINS-Fusion.git

    cd ../ # 退回到catkin_ws文件夹
    catkin build # 或 catkin_make。相当于cmake+make
    # 编译完成后,在代码运行前,须使用source命令刷新一下工作空间的环境。否则会出现找不到“package XXX not found” 的问题。
    source ./catkin_ws/devel/setup.bash

    加速编译:

    1. catkin build命令会自动执行多线程,使用最快的编译速度。

    2. catkin_make -j2 -l2 :

      • -j2,j 是 job 的意思,代表允许 2 个编译命令同时进行,一般是以 CPU的核心数目的两倍为。
      • -l2,l 是 load-average 的意思,代表系统加载的任务数,数目一般与-j的数目保持一致。

      注意,此处不仅限于2,可以是2,4,8,这取决于你是几核CPU。

    如果编译报错:

    catkin clean相当于rm -r ${build} ${devel},但是避免了rm -r这种危险的操作!

  3. 添加环境变量(可选)

    代码编译完成,后面需要运行ros命令,为了避免每次打开新的shell都需要source,我们将其添加到bash配置文件中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 方法一,使用gedit
    echosource youfolder/catkin_ws/devel/setup.bash” >> ~/.bashrc
    gedit ~/.bashrc # 注:用gedit打开文件,查看是否加入

    # 方法二,使用vim
    sudo vi ~/.bashrc # 注:打开.bashrc文件
    # 最后一行添加:
    source youfolder/catkin_ws/devel/setup.bash
    # 注:点击键盘“E”,进入编辑(edit)模式,用“delete”键删除添加的路径

    注,vim按键

    1
    2
    3
    4
    5
    6
    7
    8
    按Esc。输入 :wq!,保存并退出。
    注:ESC”键退出编辑模式,并输入以下命令:
    :wq 保存后退出vi,若为:wq!则为强制存储后退出
    :w 保存但不退出
    :w! 若文件属性为【只读】,强制写入该档案
    :q 离开vi
    :q! 若曾经修改过档案,又不想存储,使用!为强制离开不存储档案
    :e! 将档案还原到最原始状态!
  4. 运行代码

    1
    2
    3
    4
    5
    # 示例
    # 分别新建三个终端各自按顺序执行
    roslaunch vins vins_rviz.launch
    rosrun vins vins_node ~/catkin_ws/src/VINS-Fusion/config/euroc/euroc_mono_imu_config.yaml
    rosbag play ~/SLAM/test_data/MH_04_difficult.bag
  5. 完成。

知识

编译

命令

catkin是由ROS社区开发的元编译系统(meta-buildsystem),最初目的是用于编译ROS社区中多个相互依赖的CMake工程。主要有几个阶段:

  1. catkin_make。刚开始是以catkin_make为代表的命令行工具,主要是封装一系列的cmake命令。catkin_make相当于:

    1
    2
    3
    4
    mkdir build
    cd build
    cmake ../src -DCATKIN_DEVEL_SPACE=../devel -DCMAKE_INSTALL_PREFIX=../install
    make -j<number of cores> -l<number of cores> [optional target, e.g. install]

    但是catkin_make自身有些缺陷。比如,没有编译时的错误隔离。

  2. catkin_make_isolated。后来catkin_make_isolated解决了隔离的问题,但是编译速度还是慢,因为不支持并行编译。

  3. catkin tools(推荐使用)。再后来catkin tools独立出来,推出一系列python编写的编译命令。其中catkin build支持隔离和并行编译。简单来说,如果ros的工作空间 (workspace) 中只有一个ros包(ros package),那么catkin_make和catkin build区别不大;如果一个caktin workspace里有多个ros包,那么catkin build显然是更好的编译工具。因为catkin_make只是几行Cmake和make指令的简写,不管里面有几个包都一起编译了。但是catkin_build会将工作空间里所有的包同时单独(isolated)编译,编译过程互不影响。

    在编译生成的文件夹中,catkin_make会生成一个与src文件夹中各文件(夹)层级相同的ros包,而catkin build会生成各自单独的ros包?

catkin_make

  • 编译指定的包:

    1
    2
    3
    catkin_make -DCATKIN_WHITELIST_PACKAGES="package1;package2"  # 不会自动编译依赖包,也就是说需要手动添加依赖包。or
    catkin_make --only-pkg-with-deps <target_package>
    catkin_make -DCATKIN_WHITELIST_PACKAGES="" # 如果想要再回到那种catkin_make 编译所有包的状态
  • 不编译指定包。在包的根目录(CMakeLists.txtpackage.xml的同级目录)下添加名为CATKIN_IGNORE的空文件即可。如果有好几个包放在一个文件夹下,再在这个文件夹添加名为CATKIN_IGNORE的文件,则这个文件夹下所有的包都不会被编译。

    如果您现在运行 catkin_make (或任何其他 catkin 变体),它应该忽略该目录(以及所有子目录)中的任何包。请注意,如果您在添加 CATKIN_IGNORE 之前已经构建了工作区,catkin 仍可能会拾取 pkg,因为它随后会使用 build 目录中的一些信息。只需删除 build/devel/ 并重建即可解决此问题。

catkin tools

  • cmake build

    • cmake build的输出在catkin_ws/logs/package/build.cmake.log、build.make.log里。

    • 在使用cmake build进行编译时,会先调用package.xml,检测所有的依赖库都编译好了,再编译源码。如果有依赖库编译报错,源码编译就会被Abandoned。所以,当依赖库报错时,可以先不编译整个项目的源码,而是先指定cmake build编译报错的依赖库。等依赖库的报错解决了,再编译整个项目。

    • catkin clean直接cmake build只会编译修改过的包?检查到没有修改的包就跳过。

    • 跳过包。假设您按照 roslib 构建了每个包,但该包有一个构建错误。修复错误后,您可以再次运行相同的构建命令,但构建动词提供了在这种情况下节省时间的选项。如果从头重新开始,roslib 的依赖项的任何产品都不会被重新构建,但是底层构建系统仍然需要一些时间来验证每个包。但是,可以通过直接跳转到给定的包来跳过这些检查。 您可以使用 --start-with 选项在解决问题后从中断的地方继续构建。

      1
      2
      cd /tmp/ros_tutorials_ws         # Navigate to workspace
      catkin build --start-with roslib # Build roslib and its dependents
    • 编译单个包。

      1
      2
      cd /tmp/ros_tutorials_ws      # Navigate to workspace
      catkin build roslib # Build roslib

      如果您只对在工作区中构建单个包感兴趣,您还可以使用 --no-deps 选项和包名称。这将跳过包的所有依赖项,构建给定的包,然后退出。

      1
      catkin build roslib --no-deps # Build roslib only
    • 不编译指定包。

      • 在包的根目录(CMakeLists.txtpackage.xml的同级目录)下添加名为CATKIN_IGNORE的空文件即可。如果有好几个包放在一个文件夹下,再在这个文件夹添加名为CATKIN_IGNORE的文件,则这个文件夹下所有的包都不会被编译。

        如果您现在运行 catkin_make (或任何其他 catkin 变体),它应该忽略该目录(以及所有子目录)中的任何包。请注意,如果您在添加 CATKIN_IGNORE 之前已经构建了工作区,catkin 仍可能会拾取 pkg,因为它随后会使用 build 目录中的一些信息。只需删除 build/devel/ 并重建即可解决此问题。

      • 配置参数:

        1
        2
        3
        catkin config --skiplist baz  # 要设置跳过列表,可以调用该命令
        catkin config --no-skiplist # 要清除跳过列表,您可以使用 --no-skiplist 选项

        请注意,您仍然可以通过将其名称显式传递给 catkin build 来在跳过列表和构建列表上构建包。

  • catkin clean。相当于rm -r ${build} ${devel},但是避免了rm -r这种危险的操作!

  • catkin init可以初始化workspace,而且初始化后,在workspace下的任何一个子目录里使用catkin工具,都相当于在workspace文件夹下使用,它会自动感知workspace。

  • catkin config可以查看workspace的结构和参数。

  • catkin config --merge-devel可以使得每个包编译得到的devel产物都在同一个devel文件夹里,当然也可以用catkin config --isolate-devel使得各个包的devel分开。

  • catkin list可以查看工作空间中有哪些ros包。

  • 可以通过运行以下命令获取参数的详尽列表:

    1
    source ./devel/setup.zsh && rosrun ze_vio_ceres ze_vio_ceres_node --help
  • 指定使用到的gcc和g++版本:

    1
    catkin build --cmake-args -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10

编译第三方开源软件需要下载的问题

注意CmakeList.txt里有没有指定具体版本。在package.xml里也可以看到指定的版本。

其实就类似于在系统中cmakemakemake install的步骤,只不过这里的第三方库是安装在了ROS工作区里被相互调用,catkin clean后也就删除掉了,而没有安装在系统环境里。也方便使用指定版本的第三方库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# URL https://github.com/gflags/gflags/archive/v2.2.1.zip
# GIT_REPOSITORY https://github.com/jbeder/yaml-cpp
# GIT_TAG ${YAML_CPP_TAG}
# DOWNLOAD_COMMAND rm -f SuiteSparse-${VERSION}.tar.gz && wget --retry-connrefused --waitretry=1 --timeout=40 --tries 3 https://github.com/ethz-asl/thirdparty_library_binaries/raw/master/SuiteSparse-${VERSION}.tar.gz
DOWNLOAD_COMMAND ""
URL "/yourfolder/gflags-2.2.1.zip" # 使用存档时,它将自动解压缩,除非设置了 DOWNLOAD_NO_EXTRACT 选项来阻止它。
# 默认解压路径为catkin_ws/build/xxx/xxx_src-prefix/src/xxx_src
URL_MD5 4628df9eeae10ae5f0c486f1ac982fce # (可选,仅用作文件验证)与URL搭配。随便输入一个MD5码,编译时会报错当前压缩包正确的MD5码。
UPDATE_COMMAND "" # 与URL搭配。使用自定义命令覆盖下载方法的更新步骤。
DOWNLOAD_NO_EXTRACT TRUE # 允许通过为此选项传递一个布尔真值来禁用下载步骤的提取部分。
# 如果希望控制下载的归档文件的位置及其名称,可以使用以下选项
set(CMAKE_CURRENT_BINARY_DIR /yourfolder) # 放在主程序中,而不包含在ExternalProject_Add()
DOWNLOAD_DIR ${CMAKE_CURRENT_BINARY_DIR}
DOWNLOAD_NAME jsoncpp_1.8.4.tar.gz

或者,将src(这个文件是原本解压下载的第三方源码source的地方,具体名称要看CMakeLists.txt中SOURCE_DIR的设置)中的各个第三方源码都解压好,放到src对应的文件夹中。例如catkin_ws/build/xxx/xxx_src-prefix/src/xxx.tar.gz

运行

运行节点

  • roscore是运行ROS主节点的命令。也可以在另一台位于同一个网络内的计算机上运行它。但是,除了支持多roscore的某些特殊情况,roscore在一个网络中只能运行一个。

  • rosrun是ROS的基本运行命令。它用于在功能包中运行一个节点。

    1
    rosrun PACKAGE_NAME NODE_NAME
  • 如果rosrun是执行一个节点的命令,那么roslaunch是运行多个节点的概念。该命令允许运行多个确定的节点。其他功能还包括一些专为执行具有诸多选项的节点的ROS命令,比如包括更改功能包参数或节点名称、配置节点命名空间、设置ROS_ROOT和ROS_PACKAGE_PATH以及更改环境变量等。roslaunch使用*.launch文件来设置可执行节点,它基于可扩展标记语言(XML),并提供XML标记形式的多种选项。启动时会检查系统是否已经启动了roscore,如果没有,会自动启动。

    1
    roslaunch PACKAGE_NAME LAUNCH_NAME
    • 当用roslaunch命令运行多个节点时,运行中的节点的输出(INFO、ERROR等)不会显示在终端屏幕上,这会使调试变得困难。如果此时添加了--screen选项,终端上运行的所有节点的输出将显示在终端屏幕上。

      1
      roslaunch my_first_ros_pkg union.launch --screen
  • 运行rosbag play命令需要提前启动roscore

  • rosbag play命令可以放在roscoreroslaunch命令最后运行。程序读取不到数据就等着呗。

查看

rqt
界面介绍

打开rqt:

1
rqt

工具在工具栏-Plugins里:

  • Introspection

    • Node Graph:节点图
    • Package Fraph
    • Process Monitor
  • Logging

    • Console:序号、消息内容、消息等级、节点、时间戳、主题和代码所在位置
    • Logger level:设置消息显示级别
  • Topic-Topic Monitor:包含主题、数据类型、带宽、频率、值

  • Visualization

    • Image View:可视化发布的主题
    • Plot:根据主题绘制曲线
launch文件编写
  • 可视化单个图像主题:

    1
    2
    3
    4
    <launch>
    <!-- Display -->
    <node name="image_view" pkg="rqt_image_view" type="rqt_image_view" args="/package_name/image_topic"/>
    </launch>

    args参数指定了rqt_image_view界面打开时默认显示的主题,其它主题可以在界面的下拉菜单中进行切换。

  • 可视化多个图像主题:

    • 方法一:

      1
      2
      3
      4
      5
      6
      7
      8
      <launch>
      <!-- Display -->
      <!-- Node for first image -->
      <node name="image_view_1" pkg="rqt_image_view" type="rqt_image_view" args="/package_name/image_topic_1"/>

      <!-- Node for second image -->
      <node name="image_view_2" pkg="rqt_image_view" type="rqt_image_view" args="/package_name/image_topic_2"/>
      </launch>

      这样会打开两个rqt_image_view界面分别进行显示。

    • 方法二:

      首先,编写*.lanuch文件的内容为:

      1
      2
      3
      4
      <launch>
      <!-- Display -->
      <node name="rqt_gui" pkg="rqt_gui" type="rqt_gui"/>
      </launch>

      运行*.launch文件后打开的界面是rqt主界面(默认是 工具栏-Plugins-Topic-Topic Monitor)。菜单栏-Plugins-Visualization-Image View,然后为第一个Image View窗口选择合适的主题。同理,添加第二个Image View窗口和选择主题。(可选)关闭``rqt主界面窗口,只留下Image View窗口。调整Image View窗口的位置和大小。最后,菜单栏-Perspectives-Export导出*.perspectives`配置文件。

      *.perspectives配置文件放置在项目文件夹中,修改*.launch文件:

      1
      2
      3
      4
      <launch>
      <!-- Display -->
      <node name="rqt_gui" pkg="rqt_gui" type="rqt_gui" args="--perspective-file $(find package_name)/launch/visualization.perspective"/>
      </launch>

      这样,再次运行*.launch文件时,打开的就是修改好的界面。

      修改界面后,没找到可以直接保存修改的地方。需要重新导出再覆盖源*.perspective文件。

命令行

官方文档

详细的Rostopic命令系列(包括消息发布等实例操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 查看ros的默认路径
echo $ROS_PACKAGE_PATH

# 设置消息级别
rqt_logger_level # rosrun rqt_logger_level rqt_logger_level
# 查看节点的日志输出
rqt_console # rosrun rqt_console rqt_console

# 列出系统当前的节点
rosnode list

# 列出系统当前的参数
rosparam list
rostopic list -v # 通过将“-v”选项添加到rostopic list命令,可以分开发布话题和订阅话题,并将每个话题的消息类型一起显示。
# 在线修改系统参数
rosparam set /node param # https://blog.csdn.net/gwplovekimi/article/details/104373136

# 查看所有活动话题
rostopic list
# 查看特定话题的消息类型
rostopic info /topic_name # 显示有关该话题的详细信息,包括消息类型和发布者/订阅者的数量
# 查看某个话题的消息
rostopic echo /topic_name
rostopic echo /topic_name/channels[5]/values # sensor_msgs::ChannelFloat32
rostopic echo --c /topic_name # 发送完一条信息后自动清屏发送并自动发送下条数据
rostopic echo /topic_name --noarr --nostr # 排除数组(--noarr)、排除字符串(--nostr)
rostopic echo /topic_name >fiel_name.txt # 如果 topic内容太多,终端无法完全显示,则可以保存至本地文件中,再查看。
rostopic type /topic_name | rosmsg show # 查看特定话题的消息字段和数据类型

# 查看传感器发布频率
rqt # 顶部工具栏-plugins-topic monitor
rostopic hz /topic_name

# 把数据发布到当前某个正在广播的话题上
rostopic pub [topic] [msg_type] [args]

调试

CLion调试ROS项目

GitHub Copilot

  1. 我希望能够对ROS程序进行断点调试,但是,程序读取的文件来自rosbag包。那么,如何保证在程序遇到断点暂停时,rosbag包也停止播放?

    在ROS中,rosbag包的播放和程序的执行是两个独立的进程,它们之间没有直接的通信机制来同步它们的运行状态。因此,当你在程序中设置断点并使程序暂停时,rosbag包将继续播放,这可能会导致你错过一些重要的数据。

    一种可能的解决方案是使用ROS的服务(Service)来控制rosbag的播放。你可以创建一个服务,当你的程序达到断点并暂停时,这个服务会发送一个请求给rosbag,告诉它暂停播放。当你的程序继续运行时,这个服务再次发送请求,告诉rosbag继续播放。

    然而,这需要你修改rosbag的源代码,使其能够响应这些服务请求。这可能需要一些ROS编程的经验,并且可能不适用于所有情况。

    另一种解决方案是手动控制rosbag的播放。你可以在程序达到断点并暂停时,手动暂停rosbag的播放。然后,在程序继续运行时,再手动让rosbag继续播放。这种方法的缺点是需要人工干预,但它不需要修改任何代码。

    最后,你也可以考虑使用一些可视化工具,如rqt_bag,它提供了一个图形界面,可以让你更容易地控制rosbag的播放。你可以在程序达到断点并暂停时,使用这个工具来暂停rosbag的播放。然后,在程序继续运行时,再使用这个工具让rosbag继续播放。

  2. 对于调试需要读取rosbag包播放的数据来运行的ROS程序,你有哪些推荐的调试方法?

    对于需要读取rosbag包播放数据来运行的ROS程序,以下是一些推荐的调试方法:

    1. 使用ROS的rqt工具:rqt提供了一种可视化的方式来查看和理解ROS系统的运行情况。你可以使用rqt_graph来查看节点和主题的连接情况,使用rqt_plot来绘制主题数据,使用rqt_console来查看节点的日志输出等。

    2. 使用ROS的rostopic和rosnode命令:这些命令可以帮助你查看当前运行的节点和主题,以及主题的数据。例如,你可以使用rostopic echo /topic_name来查看某个主题的数据,使用rosnode info /node_name来查看某个节点的详细信息。

    3. 使用GDB或其他调试工具:如果你的ROS程序是用C编写的,你可以使用GDB或其他C调试工具来进行断点调试。你可以在程序的关键位置设置断点,然后逐步执行程序,查看变量的值,理解程序的运行流程。

    4. 使用print或ROS_INFO输出调试信息:在程序的关键位置输出一些调试信息,可以帮助你理解程序的运行情况。例如,你可以输出某个变量的值,或者输出一些提示信息来表示程序运行到了某个位置。

    5. 控制rosbag的播放:你可以使用rosbag的命令行工具来控制rosbag的播放。例如,你可以使用rosbag play --pause bagfile.bag来暂停播放,然后在需要的时候使用rosbag play --resume bagfile.bag来恢复播放。这样,你可以在程序的关键位置暂停rosbag的播放,然后逐步执行程序,查看程序处理单个数据的情况。

    6. 使用rviz进行可视化:rviz是ROS的一个3D可视化工具,可以用来可视化传感器数据、机器人的状态、计划的路径等。通过rviz,你可以更直观地理解程序的运行情况。

    以上就是一些推荐的调试方法,具体使用哪种方法,需要根据你的程序的具体情况和你的需求来决定。

ROS知识

消息通信术语

可视化:

状态图(graph)。节点、话题、发布者和订阅者之间关系可以通过状态图(graph)直观地表示。它是当前正在运行的消息通信的图形表示。但不能为一次性服务创建状态图。执行它是通过运行rqt_graph功能包的rqt_graph节点完成的。有两种执行命令:

1
2
3
rqt_graph  # 或
rosrun rqt_graph rqt_graph # 或
rqt # 执行rqt时,在菜单中选择[Plugins]→[Introspection]→[Node Graph]

以VINS-Fusion项目为例:

圆圈表示节点,正方形表示话题。

简单来说,在ROS操作系统中,节点、话题、消息、发布者和订阅者之间的关系为:

  • 一个节点既可以作为发布者(节点)对外发布(多个)话题,也可以作为订阅者(节点)订阅来自其他(多个)节点的(多个)话题。
  • ROS中最小的进程单元就是节点(node),节点之间通过主题(topic)传递消息数据(message)。

每个节点并行运行,有消息就订阅和发布,以互相通信。

计算图是ROS处理数据的一种点对点的网络形式。ROS会创建一个连接到所有进程的网络。在系统中的任何节点都可以访问此网络,并通过该网络与其他节点进行交互,获取其他节点发布的信息,并将自身数据发布到网络上。如下图所示:

  • 节点(Node)是各自独立的可执行的文件。

    • 如果需要一个可以与其他节点进行交互的进程,则需要创建一个节点,并将此节点连接到ROS网络。
    • 在通常情况下,系统包含能够实现不同功能的多个节点。每个节点具备特定的单一的功能。
  • 主节点/节点管理器(Master)负责节点到节点的连接和消息通信。

    • roscore是它的运行命令,当运行主节点时,可以注册每个节点的名字,并根据需要获取信息。
    • 没有主节点,就不能在节点之间建立访问和消息交流(如话题和服务)。
    • 节点只有在需要注册自己的信息或向其他节点发送请求信息时才能访问主节点并获取信息。通常情况下,不检查彼此的连接状态。
  • 参数服务器(Parameter Server)能够使数据通过关键词存储在一个系统的核心位置。

    • 通过使用参数,就能在运行时配置节点或改变节点的工作任务。
  • 消息(Message)。

    • 节点通过消息完成彼此间的通信。消息包含了一个节点发送到其他节点的信息数据。同时也可以基于标准消息开发自定义消息。话题、服务和动作都使用消息。

    • 消息可以是简单的数据结构,如整数(integer)、浮点(floating point)和布尔值(boolean),或者是像“geometry_msgs/PoseStamped”一样消息包含消息的简单的数据结构,或者也可以是像 “float32[ ] ranges”或“Point32[10] points”之类的消息数组结构。另外,ROS中常用的头(header、std_msgs/Header)也可以作为消息来使用。这些消息由两种类型组成:字段类型(fieldtype)和字段名称(fieldname)。下图为ROS的基本数据类型:

    • msg文件是用于话题的消息文件,扩展名为.msg;srv文件是服务使用的消息文件,扩展名为.srv;action消息文件是动作中使用的消息文件,它使用.action扩展名。

  • 话题(topic)是节点间用来传输数据的总线。

    • 每个消息都必须有一个名称来被ROS网络路由。每一条消息都要发布到相应的话题。
    • 当一个节点发送数据时,称为该节点正在向话题发布消息。节点可以通过订阅某个话题,接收来自其他节点的消息。
    • 一个节点可以订阅某个话题而不需要该节点同时发布该话题。这就保证了消息的发布者和订阅者之间的解耦,发布者和订阅者间完全无需知晓对方的存在。同一个话题可以有多个发布者与订阅者。
  • 服务(Service)。

    • 在发布话题时,正在发送的数据能够以多对多的方式交互。但当需要从某个节点获得一个请求或答应时,就不能通过话题来实现了。在这种情况下,服务能够运行两个节点间直接进行交互。
    • 此外,服务必须有唯一的名称。当一个节点提供某个服务时,所有的节点都可以通过使用ROS客户端库编写的代码与其进行通信。
    • 一个服务被分成服务服务器和服务客户端,其中服务服务器只在有请求(request)的时候才响应(response),而服务客户端会在发送请求后接收响应。与话题不同,服务是一次性消息通信。因此,当服务的请求和响应完成时,两个连接的节点将被断开。
  • 动作(Action)。

    • 是在需要像服务那样的双向请求的情况下,使用的消息通信方式,不同于服务之处是在处理请求之后需要很长的响应,并且需要中途反馈值。
    • 动作文件也非常类似于服务,目标(goal)和结果(result)对应于请求和响应。此外,还添加了对应于中途的反馈(feedback)。它由一个设置动作目标(goal)的动作客户端(action client)和一个动作服务器(action server),动作服务器根据目标执行动作,并发送反馈和结果。
    • 动作客户端和动作服务器之间进行异步双向消息通信。
  • 消息记录包(bag)是一种用于保存和回放ROS消息数据的文件格式。

    • ROS提供了可以将bag文件可视化的图形工具:rqt_bag和PlotJuggler。

节点名称(name)

一般规则

ROS服务中使用的节点、话题、消息以及ROS中使用的参数都具有唯一的名称(name):

1
2
3
4
5
6
int main(int argc, char **argv) // 节点主函数
{
ros::init(argc, argv, "node1"); // 初始化节点
ros::NodeHandle nh; // 声明节点句柄
// 声明发布者,话题名 = bar
ros::Publisher node1_pub = nh.advertise<std_msg::Int32>("bar", 10);

其中,节点的名称是/node1。如果用一个没有任何字符的相对形式的bar来声明一个发布者,这个话题将和/bar具有相同的名字。如果以如下所示使用斜杠(/)字符用作全局形式,话题名也是/bar。所以下面两句是等价的:

1
2
ros::Publisher node1_pub = nh.advertise<std_msg::Int32>("bar", 10);
ros::Publisher node1_pub = nh.advertise<std_msg::Int32>(“/bar”, 10);

但是,如果使用波浪号(~)字符将其声明为私有,则话题名称将变为/node1/bar

1
ros::Publisher node1_pub = nh.advertise<std_msg::Int32>(“~bar”, 10);

简单地两次执行相同节点将导致之前执行的节点因ROS的性质而终止,因为ROS必须具有唯一的名称。用户可以在运行时更改节点的名称,而不需要运行额外的程序或更改源代码。例如,当有前、左、右,三个摄像头,且当多次执行同名的节点时,由于节点名重复,该节点将被重复执行,因此节点会被停止。为了避免这种情况,可以采取用同一个名称运行多个不同的节点的方法。

1
2
3
4
rosrun camera_package camera_node __name:=front _device:=/dev/video0
rosrun camera_package camera_node __name:=left _device:=/dev/video1
rosrun camera_package camera_node __name:=right _device:=/dev/video2
rosrun rqt_imgae_view rqt_imgae_view

其中。name选项使用了两个下划线(__)。选项__ns__name__log__ip__hostname__master是运行节点时使用的特殊选项。我们在话题名称选项中使用了一个下划线(_),如果它是private名称,则在现有名称前加上一个下划线。

命名规则

包资源名称具有严格的命名规则,因为它们经常在自动生成的代码中使用。因此,ROS 包不能包含下划线以外的特殊字符,并且它们必须以字母字符开头。有效的名称具有以下特征:

  • 第一个字符是字母字符 ([a-z|A-Z])
  • 后续字符可以是字母数字 ([0-9|a-z|A-Z])、下划线 ( _ ) 或正斜杠 ( / )
  • 最多有一个正斜杠 ('/')

坐标变换(TF)

ROS中的坐标转换TF在描述组成机器人的每个部分、障碍物和外部物体时是最有用的概念之一。这些可以被描述为位置(position)和方向(direction),统称为姿态(pose)。在此,位置由x、y、z这3个矢量表示,而方向是用四元数(quaternion)x、y、z、w表示。四元数并不直观,因为它们没有使用我们在日常生活中使用的三元数的角度表达方式:滚动角(roll)、俯仰角(pitch)和偏航角(yaw)。但这种四元数方式不存在滚动、俯仰和偏航矢量的欧拉(Euler)方式具有的万向节死锁(gimbal lock)问题或速度问题,因此在机器人工程中人们更喜欢用四元数(quaternion)的形式。因为同样的原因,ROS中也大量使用四元数。当然,考虑到方便,它也提供将欧拉值转换成四元数的功能。

TF中的消息采用以下格式:Header用于记录转换的时间,并使用名为child_frame_id的消息来表示下位的坐标。并且为了表达坐标的转换值,使用transformTranslation.x / transform.translation.y / transform.translation.z / transform.rotation.x / transform.rotation.y / transform.rotation.z / transform.rotation.w 的数据形式描述对方的位置和方向。

1
2
3
Header header  #记录转换的时间
string child_frame_id #表示下位的坐标
Transform transform #表达坐标的转换值

文件系统

如上图所示,首先是最顶层的catkin工作空间,它是整个ROS工程中层次最高的概念。工作空间也就是管理和组织ROS工程项目文件的地方。其下主要的一级目录有四个:

  • ./src/:源空间,存放功能包(package)。功能包是ROS文件系统中组织程序文件的基本单元,也就是catkin编译的基本单元。一个 package 下必须包含 CMakeLists.txtpackage.xml 两个文件。
  • ./build/:编译空间。存放CMake和catkin的缓存信息、配置信息和其他中间文件。
  • ./devel/:开发空间。存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件(./catkin_ws/devel/lib/package_name)等。
  • ./install/:安装空间。开发完成后的安装包。

其中,

  • 最顶层的工作空间(可以任意命名,如catkin_ws)和 src (必须为src)文件夹是需要自己创建;
  • builddevel 文件夹由 catkin buildcatkin_make 命令自动创建;
  • install 文件夹由 catkin_make install 命令自动创建。

src文件夹中内容的详解:

  1. ./include/

    • 项目程序的头文件(夹)(.h文件)。
  2. ./src/

    • 项目源程序文件(夹)(.c/.cpp文件等)。
  3. ./dependencies.yaml

    • dependencies.yaml文件列出了一个源代码项目在构建和运行时所依赖的外部库。这些库通过catkin构建系统进行管理,通常用于ROS(Robot Operating System)项目。每一个依赖项提供了三个关键信息:类型、URL和版本。在这个文件中,所有依赖项都是从Git仓库中获取的,且均指定为master分支。
    • 这个文件主要用于配置项目的依赖管理系统,确保所有所需的外部库均能被正确拉取并集成到项目中。这对于确保代码的一致性和可复现性是非常重要的。
  4. CMakeLists.txt

    • ROS 构建系统的catkin基本上使用了CMake,因此在功能包目录的CMakeLists.txt文件中描述着构建环境。
  5. .launch

    • 这是一个ROS(Robot Operating System)的启动配置文件,用于启动ROS节点。
  6. package.xml

    • 此文件是一个ROS(Robot Operating System)软件包的清单文件,用于定义包的基本信息和依赖关系。包含功能包信息的XML文件,描述功能包名称、作者、许可证和依赖包。
    • 这个文件主要被ROS用来理解如何构建和维护程序包,并且定义了必要的信息,使得包可以在ROS生态中被正确找到和编译。
  7. ./config/

    • 存放配置文件(.yaml等)。
  8. .yaml

    • 项目参数配置文件。
  9. .cfg

    • 一个配置脚本。例如:这个脚本使用 dynamic_reconfigure 功能,来允许实时修改运行中ROS节点的参数。
    • 在最后,脚本执行 generate 函数来创建一个用于动态配置的服务,该服务可以在ROS运行时动态调整上述参数。
  10. .perspective

    • 这是一个JSON格式的配置文件,用于定义一个名为ESVO的软件项目中的esvo_time_surface插件的窗口布局和行为。基于文档结构,这看起来像是为图形用户界面(GUI),如rqt插件制定的配置。rqt是一个基于Qt的软件框架,它使得为机器人操作系统(ROS)创建丰富的图形界面变得简单。
    • 总的来说,这个配置文件为ESVO项目中的esvo_time_surface.perspective插件提供了窗口布局、插件状态和用户界面元素的详细信息。
  11. .rviz

    • .rviz文件是一个用于配置 ROS (Robot Operating System) 可视化工具 RViz 的视图配置文件。RViz 是一个用来对机器人传感器数据、导航状态以及其他信息进行三维可视化的工具。这个文件定义了RViz中的一系列面板、显示项、视图以及工具的属性和设置。
  12. ./scripts/

    • 存放脚本文件(比如Python文件.py,shell文件.sh)。
  13. 此外,还有自定义的通信格式文件,包括消息(.msg)、服务(.srv)以及动作(.action)。

roslaunch

launch 文件

.launch文件中根据XML的编写方式可以实现多种功能,可使用的标签如下所示:

  • <launch>roslaunch语句的开始和结束。
  • <node>这是对于节点运行的标签。您可以更改功能包、节点名称和执行名称。
  • <machine>可以设置运行该节点的PC的名称、address、ros-root和ros-package-path。
  • <include>您可以加载属于同一个功能包或不同的功能包的另一个launch,并将其作为一个launch文件来运行。
  • <remap>可以更改节点名称、话题名称等等,在节点中用到的ROS变量的名称。
  • <env>设置环境变量,如路径和P(很少使用)。
  • <param>设置参数名称、类型、值等。
  • <rosparam>可以像rosparam命令一样,查看和修改loaddumpdelete等参数信息。
  • <group>用于分组正在运行的节点。
  • <test>用于测试节点。类以于<node>,但是有可以用于测试的选项。
  • <arg>可以在launch文件中定义一个变量,以便在像下面这样运行时更改参数。

例如,.launch文件的内容如下所示:

1
2
3
4
5
6
<launch>
<node pkg="my_first_ros_pkg" type="topic_publisher" name="topic_publisher1"/>
<node pkg="my_first_ros_pkg" type="topic_subscriber" name="topic_subscriber1"/>
<node pkg="my_first_ros_pkg" type="topic_publisher" name="topic_publisher2"/>
<node pkg="my_first_ros_pkg" type="topic_subscriber" name="topic_subscriber2"/>
</launch>
  • <launch>标签中,描述了使用roslaunch命令运行节点所需的标签。
  • <node>描述了roslaunch运行的节点。选项包括:
    • pkg:功能包的名称。
      • 节点的包名称是在ROS的package.xml文件中定义的。每个ROS包都有一个package.xml文件,它包含了包的元数据,如包的名称、版本、维护者、依赖关系等。
    • type:实际运行的节点的名称(节点名)。
      • 这是节点的类型,也就是节点的可执行文件的名称。
      • 节点的可执行文件名称是在ROS包的CMakeLists.txt文件中定义的。在CMakeLists.txt文件中,add_executable()函数用于创建一个可执行文件,target_link_libraries()函数用于链接必要的库。
    • name:这是节点的名称,它在ROS系统中必须是唯一的。
      • 一般与在主程序中在ros::init(argc, argv, "node_name");初始化定义的节点名称相同。
      • 与上述type对应的节点被运行时,起的名称(运行名)。一般情况下使用与type相同的名称,但可以根据需要,在运行时更改名称。在上面的示例代码中,topic_publisher节点已被重命名为topic_publisher1topic_publisher2,而topic_subscriber节点已被重命名为topic_subscriber1topic_subscriber2。可以由此避免两次执行相同节点导致之前执行的节点因ROS的性质而终止。

打开rqt如下图所示:

两个订阅者都在订阅两个发布者的消息。这是因为我们只是改变了节点的名字,而没有改变要订阅的消息的名字。

修改前面所创建的.launch文件如下:

1
2
3
4
5
6
7
8
9
10
11
<launch>
<group ns="ns1">
<node pkg="my_first_ros_pkg" type="topic_publisher" name="topic_publisher"/>
<node pkg="my_first_ros_pkg" type="topic_subscriber" name="topic_subscriber"/>
</group>

<group ns="ns2">
<node pkg="my_first_ros_pkg" type="topic_publisher" name="topic_publisher"/>
<node pkg="my_first_ros_pkg" type="topic_subscriber" name="topic_subscriber"/>
</group>
</launch>

<group>是对指定节点进行分组的标签。选项有ns。这是命名空间(name space),是组的名称,属于该组的节点和消息都包含在由ns指定的名称中。

再次打开rqt结果如下图所示:

命令行参数

1
2
3
4
5
6
7
8
<launch>
<arg name="auto_trigger" default="true"/>
<arg name="camera_name" default="DAVIS-evo" />
<arg name="bootstrap_image_topic" default="/events/image_raw" />

<param name="camera_name" value="$(arg camera_name)" />
<param name="calib_file" value="$(find dvs_tracking)/parameters/calib/$(arg camera_name).yaml" />
</launch>
1
source ./devel/setup.zsh &&  roslaunch dvs_tracking [launch-file] bootstrap_image_topic:=/dvs/image_raw auto_trigger:=true camera_name:=DAVIS-ijrr

CMakeLists.txt 和 package.xml

参考链接

话题通信机制、消息队列与多话题回调机制

  1. 话题通信机制简介:

    1. 首先,发布节点把消息发布,消息进入Publisher的消息队列,同时通知订阅了该话题消息的Subscriber来取消息。
    2. 其次,Subscriber来Publisher的消息队列里取消息,但取走的也是最老的消息,因为毕竟这是先入先出的队列。
    3. 最后,被取走的消息存放入了Subscriber的消息队列中,等待被Callback执行。如果Callback执行很慢,消息越堆越多,最老的消息会逐渐被顶替。
  2. 消息队列分析:

    Publisher和Subscriber设置消息队列,都可以从两点进行分析:

    • 话题通信属于异步通信,队列可以存储一定量的历史数据。
    • 网络传输异常的时候,队列可以预先进行缓存。
    1. 为什么需要设置Publisher的消息队列?
      • 话题通信属于异步通信,publisher节点发布消息,有多个Subscriber节点进行订阅,因此需要有一个地方对消息进行缓存。
      • 网络传输差、延时突然高的时候,可以把消息放在Publisher的队列中进行暂存。
    2. 为什么要设置Subscriber消息队列?
      • Subscriber消息队列提供一边处理数据一边缓存新消息的机制。Publisher和Subscriber不一定在同一台主机上,但是网络的性能时好时坏,如果Subscriber没有消息队列。如果没有Subscriber消息队列,那么程序每次运行Callback函数前都要先通过网络取回消息,然后才能处理,当网络很差的时候就会造成系统的阻塞。
  3. 使用技巧:

    • 当queue_size=0,即ROS消息队列为0时,表示为无限队列长度,内存使用量可以无限增长,因此不推荐使用。
    • 当两个queue_size=1时,那么系统不会缓存数据,自然处理的就是最新的消息。
    • 当queue_size设置为10或者更多时候,用户更不容易错过发布的消息,适用于与人交互的用户界面的数据展示。
  4. 多话题回调机制:

    主要涉及到两个函数的使用:ros::spin()ros::spinOnce()

    1. ros::spin()

      • 对于spin函数,一旦进入spin函数,它就不会返回了,也不继续往后执行了,相当于它在自己的函数里面死循环了(直到Ctrl+C 或者程序终止的时候才退出)。
      • 主要的工作,就是不断的检查回调函数队列里面是否有callback函数存在,如果有的话,它就会马上去执行callback函数。如果没有的话,它就会阻塞,不会占用CPU。

      ros::spin()可以看作r.sleep()=0ros::spinOnce(),即:

      1
      2
      3
      4
      while(ros::ok())
      {
      ros::spinOnce();
      }

      其他方面都可当作spinOnce()来理解。

    2. ros::spinOnce()

      • spinOnce函数执行一次后,接着执行主函数下面的语句。不像spin函数,进入到自己的内部循环,不往下执行。即可以根据自己的需求设置接收频率。
      • 每次调用ros::spinOnce()都会执行与消息队列中缓存的信息数量相同次数的回调函数,只要回调函数执行够快的话,就能清空队列。
      • 当多个subscriber都需要调用回调函数时,则按顺序依次执行各个subscriber的回调函数。例如,订阅的消息队列长度分别为5和3,那么以0.1hz即10s的间隔调用ros::spinOnce()时,两个Topic的消息队列都满了,这时一共会调用8次回调函数,且前6次为交替调用。就是说这些回调函数都是串行执行的。
      • ros::spinOnce()可以配合其它函数,一起放在while循环中处理。也就是说,当程序中除了响应回调函数还有其他重复性工作的时候,那就在循环中做那些工作,然后调用ros::spinOnce()

      spinOnce()的主要用法是:

      1
      2
      3
      4
      5
      6
      ros::Rate r(rate);  // 设置执行频率。
      while (ros::ok())
      {
      ros::spinOnce();
      r.sleep(); // 配合执行频率,sleep一段时间,然后进入下一个循环。
      }

      有一点要注意,在回调函数里执行的时间也是会算在r.sleep()中的,如果在回调函数中执行的时间大于r.sleep()需要延时的时间,那么回调函数就一直执行下去。

    3. ros::MultiThreadedSpinner

      MultiThreadedSpinner的用法是:

      1
      2
      ros::MultiThreadedSpinner spinner(4); // 数字表示调用的线程数  
      spinner.spin(); // spin() will not return until the node has been shutdown

      通过MultiThreadedSpinner调用的回调函数就不再是串行执行的了,而是多线程并行执行。

    4. 总结:

      1. spinOnce()/spin()调用时是串行执行所有的回调函数,并且是各个话题按顺序执行的,执行的次数与调用spinOnce()当时消息队列的元素个数相同。
      2. MultiThreadedSpinner调用的回调函数就不再是串行执行的了,而是多线程并行执行。
    5. 注意事项:

      在单话题回调时,必然只存在一个消息队列。但若是多话题回调,又该如何呢?

      1. 多个话题的订阅器是否共享一个消息队列?在默认情况下,所有的回调函数都会被分配到一个全局队列中,然后由spin()或其它函数进行处理。也就是说,多话题的订阅的消息是共享一个全局消息队列的。根据上文spinOnce()的描述,在执行spinOnce()时会处理当时在全局队列中所有的消息。若当时队列中存在多个话题消息,那么就会处理多个话题的回调函数。但由于是单线程的,因此处理的过程是串行的。
      2. 若它们共享一个消息队列,那为什么每个订阅器在初始化时都可以设置队列长度?各个订阅器设置的队列长度是指该订阅话题在整个全局队列中所占的长度。也就是说,全局队列的长度至少不会小于所有话题订阅器设置的队列之和。
      3. 是否可以自定义设置多个消息队列?可以自定义设置消息队列,roscpp中提供了相应函数和类。详情可参照[官方文档](https://wiki.ros.org/roscpp/Overview/Callbacks and Spinning)。
  5. 注意事项:

    • 在执行回调函数时,消息队列并不会被锁定住,会继续更新。
    • 当回调函数处理时长小于数据发布的时间间隔,数据可以完整地传输。
    • 同一平台上,发布与订阅节点之间存在延迟时间:大概为0.2ms。
    • 先运行订阅者节点,后运行发布者节点也会存在数据丢失,说明:订阅者与发布者刚建立连接时需要大致0.5s的时间。
    • 数据发布到缓存到订阅者队列的时间是很短的,大约为0.5ms,如果对数据实时性要求比较高或只想要处理当前最新的消息时,发布者和订阅者的队列长度均需要设置为1。
    • 回调函数处理数据时间过长,subscriber队列数据堆积,并可能导致数据丢失。每次执行回调函数时,会从subscriber队列选取最老的数据。
    • 如果我们想完整的接收所有的消息,那就要设置合适的队列长度了。比如消息发送频率为10hz,接收频率为5hz,那订阅队列就至少为2(5x2=10)。

发布者节点和订阅者节点的创建和运行

订阅:

1
2
ros::Subscriber 订阅者 = n.subscribe(订阅的主题, 信息队列大小, 回调函数);  # 声明
void 回调函数() # 定义

发布:

1
2
3
ros::Publisher 发布者;  # 声明
发布者 = n.advertise<消息类型>(发布主题, 信息队列大小);
发布者.publish(要发布的消息); # 定义

函数语法

订阅函数:

1
2
3
4
5
Subscriber ros::NodeHandle::subscribe    (    const std::string & topic,
uint32_t queue_size,
void(*)(M) fp,
const TransportHints & transport_hints = TransportHints()
)

其中的参数:

  • topic 为订阅的节点名,字符串类型。
  • queue_size 为待处理信息队列大小。
  • fp 当消息传入时,可以调用的函数指针,即回调函数。而其中M是回调函数的不同类型,例如const boost::shared_ptr<M const>& or const M&。这样的话,我们还可以使用boost::bind()调用回调函数。

常见的用法:

1
2
3
4
5
void callback(const std_msgs::Empty::ConstPtr& message)
{
}

ros::Subscriber sub = handle.subscribe("my_topic", 1, callback);

当我们要使用一个class里面的回调函数(而不是,例如,使用的是直接定义在main()外的回调函数),我们需要调用第四个参数来声明使用到的类。例如我们有一个class

1
2
3
4
5
class Listener
{
public:
  void callback(const std_msgs::String::ConstPtr& msg);
};

如果想要调用这个class里的回调函数,可以使用第四个参数,如下:

1
2
Listener listener;
ros::Subscriber sub = n.subscribe("chatter", 1000, &Listener::callback, &listener);

如果订阅的函数也在class里面(例如,在类的构造函数里),可以直接使用出参数this

1
2
3
4
5
6
7
8
9
10
11
12
class Listener
{
public:
  void callback(const std_msgs::String::ConstPtr& msg){}

  void TestObject(ros::NodeHandle n)
  {
    
ros::Subscriber sub = n.subscribe("chatter", 1000, &Listener::callback, this);
  }

};

创建

topic_publisher.cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "ros/ros.h" // ROS默认头文件
#include "my_first_ros_pkg/MsgTutorial.h" // MsgTutorial消息头文件(构建后自动生成)
//千米已经定义了这个消息,相当于引用这个消息



int main(int argc, char **argv) // 节点主函数
{
ros::init(argc, argv, "topic_publisher"); // 初始化节点名称

ros::NodeHandle nh; // 声明一个节点句柄来与ROS系统进行通信


// 声明发布者,创建一个使用my_first_ros_pkg功能包的MsgTutorial 消息文件的
// 发布者ros_tutorial_pub。话题名称是"ros_tutorial_msg",
// 消息文件发布者队列(queue)的大小设置为100
ros::Publisher ros_tutorial_pub =nh.advertise<my_first_ros_pkg::MsgTutorial>("ros_tutorial_msg", 100);//话题名称将在订阅者中重现

// 设定循环周期。"10"是指10Hz,是以0.1秒间隔重复
ros::Rate loop_rate(10);


my_first_ros_pkg::MsgTutorial msg; // 以MsgTutorial消息文件格式声明一个 叫做msg的消息
int count = 0; // 声明要在消息中使用的变量
while (ros::ok())
{
//消息文件中的两个元素
msg.stamp = ros::Time::now(); // 把当前时间传给msg的下级消息stamp
msg.data = count; // 将变量count的值传给下级消息data

//屏幕上显示的内容仅仅是使用ROS专用的ROS_INFO()函数将信息显示在屏幕上,这个函数与常见编程语言中使用的printf()函数类似。
ROS_INFO("send msg = %d", msg.stamp.sec); // 显示stamp.sec消息
ROS_INFO("send msg = %d", msg.stamp.nsec); // 显示stamp.nsec消息
ROS_INFO("send msg = %d", msg.data); // 显示data消息
ros_tutorial_pub.publish(msg); // 发布消息。
loop_rate.sleep(); // 按照上面定义的循环周期进行暂歇
++count; // 变量count增加1
}
return 0;
}

topic_subscriber.cpp文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "ros/ros.h" // ROS默认头文件
#include "my_first_ros_pkg/MsgTutorial.h" // MsgTutorial消息头文件( 构建后自动生成)
//千米已经定义了这个消息,相当于引用这个消息


// 这是一个消息后台函数,
// 此函数在收到一个下面设置的名为ros_tutorial_msg的话题时候被调用。(在发布者中定义了))
// 输入的消息是从my_first_ros_pkg功能包接收MsgTutorial消息。
void msgCallback(const my_first_ros_pkg::MsgTutorial::ConstPtr& msg)
{
ROS_INFO("recieve msg = %d", msg->stamp.sec); // 显示stamp.sec消息
ROS_INFO("recieve msg = %d", msg->stamp.nsec); // 显示stamp.nsec消息
ROS_INFO("recieve msg = %d", msg->data); // 显示data消息
}


int main(int argc, char **argv) // 节点主函数
{
ros::init(argc, argv, "topic_subscriber"); // 初始化节点名称
ros::NodeHandle nh; // 声明用于ROS系统和通信的节点句柄

// 声明订阅者,创建一个订阅者ros_tutorial_sub,
// 它利用my_first_ros_pkg功能包的的MsgTutorial消息文件。
// 话题名称是"ros_tutorial_msg",订阅者队列(queue)的大小设为100。
ros::Subscriber ros_tutorial_sub = nh.subscribe("ros_tutorial_msg", 100, msgCallback);

// 用于调用后台函数,等待接收消息。在接收到消息时执行后台函数。
ros::spin();
return 0;
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 运行发布者
roscore
rosrun my_first_ros_pkg topic_publisher # my_first_ros_pkg为cmake创建的包名。

# 查看ROS网络当前正在使用的话题列表
rostopic list
# 使用rostopic命令获取topic_publisher发布的话题
rostopic echo /ros_tutorial_msg

# 运行订阅者
rosrun my_first_ros_pkg topic_subscriber

# 查看运行中的节点的通信状态
rqt_graph # 或
rosrun rqt_graph rqt_graph # 或
rqt # 执行rqt时,在菜单中选择[Plugins]→[Introspection]→[Node Graph]。

创建和运行服务服务器与客户端节点

参考链接

创建和运行动作服务器与客户端节点

参考链接

rosbag

获取当前工作目录下bag文件的信息:

1
2
3
4
5
6
7
rosbag info BagFileName.bag
# 以yaml格式显示bag文件的信息
rosbag info -y BagFileName.bag
# 基于yaml信息表示形式,提取bag文件的某类信息
# 可提取的信息类型为:消息的类型types、话题相关信息topics、bag文件存放路径path、ROS的版本信息vision、录制持续时长duration、录制开始时间start/停止时间end、bag文件大小(以byte为单位)、录制消息总数量messages、文件的压缩格式compression、是否添加至索引列表indexed。
rosbag info -y -k info_type BagFileName.bag
rosmsg show geometry_msgs/PoseStamped # 主题消息的各数据类型

回放:

1
2
3
4
5
6
7
rosbag play record.bag  # 在回放过程中按空格暂停
# -r 指定播放速度 2倍速播放: rosbag play -r 2 xx.bag
# -l 表示loop,循环播放。
# -s参数用于指定从几秒开始: 从10秒开始播放:rosbag play -s 10 xx.bag
# -u参数表示仅播放包的前几秒信息: 仅播放前十秒:rosbag play -u 10 xx.bag
# 从120s开始播放,只播放20s,就是播放120s~140s的时间段:rosbag play xx.bag -s 120 -u 20
rosbag play record.bag -r 0.1 -l

可视化bag文件:

1
2
3
4
5
6
7
8
# 使用rqt_bag
roscore
rqt_bag BagFileName.bag
# 使用rviz
roscore
rosbag play record.bag
rosrun rviz rviz
# 点击rviz左下角的Add,添加想可视化的数据类型

查询:

1
2
3
4
5
6
# 查看频率
rostopic hz /topic # 与rosbag包的播放倍率有关。
# 列出当下项目文件中所有通信的主题
rostopic list
# 查看通过指定topic话题相互通信的双方的信息
rostopic info topic_name

编辑:

1
2
3
4
5
6
# 更改消息频率(配合下方record录制功能录制outtopic的bag包)
rosrun topic_tools throttle messages <intopic> <msgs_per_sec> [outtopic] # outtopic不输入默认为intopic
# 例如,将/dvs/image_raw变为4.0Hz的/color。
rosrun topic_tools throttle messages /dvs/image_raw 4.0 /color

rosbag play MH_01_easy.bag /old/topic:=/new/topic # 替换 topic name

录制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 订阅主题并写入一个包文件,其中包含在这些主题上发布的所有消息的内容。
# 默认路径:如果您不指定路径,rosbag record 将在当前用户的主目录(通常是 /home/username)下创建一个 .bag 文件。文件名将包含时间戳作为后缀。
# 指定路径:在特定路径下打开终端;或者使用 -o 参数指定路径
# -o yourfolder/xxx -> yourfolder/xxx_time.bag
# -o yourfolder/xxx/ -> yourfolder/xxx/_time.bag
rosbag record TOPIC1 [TOPIC2 TOPIC3 ...]
# 参数:
-h, --help 显示此帮助消息并退出
-a, --all 记录所有主题
-e, --regex 使用正则表达式匹配主题
-p, --publish 在记录开始时发布一条消息
-x EXCLUDE_REGEX, --exclude=EXCLUDE_REGEX 排除与以下正则表达式匹配的主题(从 -a 或正则表达式中减去)
-q, --quiet 抑制控制台输出
-o PREFIX,--output-prefix=PREFIX 将 PREFIX 添加到包名称的开头(名称将始终以日期戳结束)
-O NAME, --output-name=NAME 记录到名为 NAME.bag 的包中
--split 当达到最大大小或持续时间时分割bag包
--max-splits=MAX_SPLITS 最多保留N个包文件,当达到最多擦除最旧的一个以保持常数文件数量
--size=SIZE 记录一个包的最大尺寸为SIZEMB。 (默认:无穷)
--duration=DURATION 记录一个包的最大持续时间DURATION,以秒为单位,除非附加“m”或“h”
-b 大小,--buffsize=大小 使用 SIZE MB 的内部缓冲区(默认值:256,0 =无穷)
--chunksize=SIZE 高级用法。 记录到 SIZE KB 的块(默认值:768)
-l NUM, --limit=NUM 每个主题仅记录 NUM 条消息
--node=NODE 记录特定节点订阅的所有主题
-j, --bz2 使用 BZ2 压缩
--lz4 使用LZ4压缩
--tcpnodelay 订阅时使用 TCP_NODELAY 传输提示主题
--udp 订阅主题时使用 UDP 传输提示

编写*.launch文件:

1
2
3
<launch>
<node pkg="rosbag" type="record" name="bag_record" args="/davis_left/events /davis_left/image_raw /davis_left/imu -o /yourfolder/record"/>
</launch>

rosclean

清理文件系统资源(例如日志文件)。

1
2
3
4
5
Usage: rosclean <command>

Commands:
rosclean check Check usage of log files 报告 ROS 日志文件的磁盘使用情况。
rosclean purge Remove log files 小心使用。这将从磁盘中删除文件!将删除与存储 ROS 相关日志文件相关的目录。系统会要求您确认每次删除,并且验证 rosclean purge 执行的命令是否正确非常重要。否则您可能会丢失重要文件。

时间戳

知识

在ROS(机器人操作系统)中,时间戳(Timestamp)是一个重要的概念,用于在消息传递和数据同步中跟踪和标记事件的时间。ROS使用一种称为ROS::Time的数据结构来表示时间戳,通常与ROS消息一起使用。以下是有关ROS时间戳的一些关键信息:

  • 数据结构

    ROS::Time 是ROS中表示时间戳的数据结构。它包括两个成员变量:sec(秒)和nsec(纳秒)。这两个成员变量的组合提供了一个精确的时间戳,精度为纳秒级。例如,ros::Time(1633895107, 123456789) 表示的时间戳为1633895107秒 + 123456789纳秒。

  • 时钟来源

    在roscpp(C++)和rospy(Python)中,可以使用相应的API来获取当前时间、计算时间差、比较时间戳等。例如在C++中,ros::Time::now() 获取当前时间戳,在Python中,rospy.get_rostime()获取当前时间戳。

    时间信息的来源有两种,系统时间和仿真时间,分别被称为Wall Time和ROS Time,可以通过参数use_sim_time来进行选择,当use_sim_timetrue时选择使用仿真时间,反之使用系统时间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 设置参数use_sim_time:
    # 通过节点设置:
    # 使用仿真时间
    rosparam set use_sim_time true
    # 使用系统时间
    rosparam set use_sim_time false

    # launch文件设置:
    <param name="use_sim_time" value="false" />

    系统时间直接通过访问本地时钟来获得,而仿真时间则需要订阅/clock话题的消息获得,每当接收到该话题的消息,时间才会更新,在没有接收到该话题的第一个消息时,当前时间为0,/clock是通过仿真时间服务器发布的,通常为ros_bag节点或仿真器。

    此外,ROS的绝对时间戳ROS::WallTime,它不可以被人为修改,不同话题或服务数据的ROS::Time时间戳会和绝对时间戳ROS::WallTime有一个差值,如果需要满足较高精度的对齐,需要进行时间补偿。

  • 应用

    在ROS中,每个消息类型通常一般都包含一个时间戳字段,例如Header消息类型包括stamp字段,用于存储时间戳。这允许节点记录和传递消息的时间信息。使用ROS时间戳,可以实现多个节点之间的数据同步。

  1. 为什么有时候ros:time::now()输出值为0?

    ROS设置了一个模拟时钟的节点,使用模拟时钟的时候,now()返回时间0直到第一条消息在/clock已经收到,所以当客户端不知道时钟时间时ros:time::now()输出为0。

  2. ros:time::now()输出的值是什么?

    ros:time::now()输出的值与参数use_sim_time有关。

    • use_sim_timefalse时,ros:time::now()输出系统时间;

    • use_sim_timetrue时,ros:time::now()输出输出仿真时间,如果回放bag则是bag的时间。

  3. 如何直接使用系统时间?

    使用ros::WallTime::now() 。播放rosbag时,若参数/use_sim_timetrue,则此时:

    • ros::WallTime::now()为当前的真实时间,也就是墙上的挂钟时间,一直在走。
    • ros::Time::now()rosbag当时的时间,是由bag/clock获取的。是仿真时间。

使用仿真时间

你可以通过设置/use_sim_time参数为true并在播放rosbag时使用--clock选项来让ROS使用rosbag的时间,而不是真实时间。这种方法通常用于回放rosbag时的仿真。

首先,你需要在启动你的节点之前,设置/use_sim_time参数为true

1
rosparam set /use_sim_time true

或者,在*.launch文件中指定:

1
2
3
4
5
6
7
8
<launch>
<rosparam param="/use_sim_time">true</rosparam>

<node name="name" pkg="pkg" type="type" output="screen">
<param name="topic" type="string" value="/topic"/>
</node>

</launch>

不执行上述操作,时间戳就还是现实时间,不是 rosbag 的时间戳。

然后,当你播放rosbag时,需要添加--clock选项:

1
rosbag play --clock your_rosbag.bag

这样,ROS的时间就会同步到rosbag的时间。然后,你可以在你的代码中使用ros::Time::now()来获取当前的rosbag时间,而不是真实时间。

1
2
cv_bridge::CvImage optical_flow_msg;
optical_flow_msg.header.stamp = ros::Time::now();

消息打印

基本用法

ROS的Log等级跟平常的一样:DEBUG、INFO、WARN、ERROR、FATAL。

引入头文件:

1
#include <ros/console.h>
  1. 类似C语言风格(类比printf

    1
    ROS_INFO("position=(%0.2f,%0.2f) direction=%0.2f", msg.x, msg.y, msg.theta);
  2. 类似C++语言风格(类比 std::cout)(推荐使用)

    • 输出多个变量和设置小数点位数

      1
      ROS_INFO_STREAM( std::setprecision (2) << std::fixed<< " position=(" << msg.x << " ," << msg.y << ")" << "direction=" << msg.theta );
    • 为调试信息命名

      1
      ROS_INFO_STREAM_NAMED( “named_msg”, “INFO named message.” );  // 为这段信息命名,可以更容易知道这段信息来自哪段代码
    • 打印一次与定时打印

      1
      2
      3
      4
      ROS_INFO_STREAM_ONCE(recv head1 : -> << std::hex << static_cast<int>((int)buffer_data[0]));  // 放在回调函数中确认数据有接收到,但是又不想一直刷屏可以用这个

      // ROS_[_STREAM]_ONCE[_NAMED]
      ROS_INFO_STREAM_THROTTLE(0.5, "Message print every 0.5s"); // 定时刷屏有时候回调的频率比较高,可以设置慢一点的打印。每0.5s打印一次。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <ros/ros.h>
#include <ros/console.h>

int main( int argc, char **argv )
{
ros::init( argc, argv, "rosdebug" );
ros::NodeHandle n;
ros::Rate rate( 1 );

while( ros::ok() ) {
ROS_INFO_STREAM_ONCE("loop start");
ROS_DEBUG_STREAM( "DEBUG message." );
ROS_INFO_STREAM ( "INFO message." );
ROS_WARN_STREAM ( "WARN message." );
ROS_ERROR_STREAM( "ERROR message." );
ROS_FATAL_STREAM( "FATAL message." );
ROS_INFO_STREAM_NAMED( "named_msg", "INFO named message." );
ROS_INFO_STREAM_THROTTLE( 2, "INFO throttle message." );
ros::spinOnce();
rate.sleep();
}
return 1;
}

时间戳

1
2
3
4
# 取消仿真时间
rosparam set use_sim_time false
# 设置使用仿真时间
rosparam set use_sim_time true

输出:

use_sim_time false时,输出时间戳为系统时间:

1
[INFO] [1648006025.447472]: Hello world!

当设置use_sim_time true时执行程序:

1
[INFO] [1648006222.430755, 0.000000]: Hello world!

输出信息:第一个为系统时间戳,第二个为消息的输出的时候时钟源的时间,当ros系统设置了use_sim_timetrue的时候,这个为播放的bag包的时间,没有bag数据包播放时仿真时间为0,会卡住。

如果设置了ros时钟仿真参数,发布一个时间的clock话题,就可以控制ros系统的时间。找个bag数据包,播放一下,使用仿真时间加 —clock

1
rosbag play filename.bag --clock

程序继续运行输出如下,时间戳多了仿真时间一项:

1
[INFO] [1648006373.102147, 1647073793.956939]: Hello world!

但是当发布clock话题的节点没有运行,会出现什么反应?其他所有节点线程遇到延时,执行rospy.sleeprate.sleep等函数时,会一直卡住,while循环不跳出。一直等待 ,直到时钟初始化。

另外,你无法直接分辨是哪天,这都是从1970-01-01 00:00:00 UTC开始算起的时间,单位为秒。国内北京时间UTC+8.0所以时间戳是从1970-01-01 08:00:00开始。

要想看懂时间戳,下面就轮到rqt_console登场了。

使用rosconsole和设置显示级别

ROS的调试信息可以用rosconsole查看和设置,用法如下:

  1. 查看节点:

    1
    rosnode list
  2. 查看节点的log(例如,节点名为rosdebugtest):

    1
    2
    3
    4
    5
    6
    7
    8
    rosconsole list rosdebugtest
    # 输出为:
    ros
    ros.roscpp
    ros.roscpp.roscpp_internal
    ros.roscpp.superdebug
    ros.rosdebugtest
    ros.rosdebugtest.named_msg
  3. 查看log显示级别:

    1
    2
    # Usage: rosconsole get
    rosconsole get rosdebugtest ros.rosdebugtest.named_msg
  4. 动态设置log的显示级别:

    1
    2
    # Usage:rosconsole set <node> <logger> <level>
    rosconsole set rosdebugtest ros.rosdebugtest debug

    或者用图形化界面设置,启动rqt_logger_level

    1
    rosrun rqt_logger_level rqt_logger_level

    找到相应的node名,选择loggers,等级调整,默认为Inf

  5. 在编译时设置源代码的调试级别:

    由于编译时默认显示的是INFO级别,因此调试时为了看到Debug信息,需要在编译是设置调试级别。

    ros::init( argc, argv, “rosdebugtest” );后加入这一句就行了:

    1
    ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME,ros::console::levels::Debug);
  6. 设置ros默认输出级别:

    更改文件$ROS_ROOT/config/rosconsole.config,文件内容如下:

    1
    2
    3
    4
    # Set the default ros output to warning and higher
    log4j.logger.ros=WARN
    # Override my package to output everything
    log4j.logger.ros.my_package_name=DEBUG

打开rosconsole界面:

1
rqt_console
  • 中间的Stamp列显示的就是转换过后的时间戳。
  • 第一行从左到右如图,很清晰,最后一列Location可以定位到来自于哪个文件多少行,双击每条消息任意位置可以查看消息的所有详细信息。
  • 第二行是配置过滤哪些消息,可根据等级,消息内容,时间范围,节点以及话题来滤除。
  • 第三行是配置高亮哪些消息,可根据等级,消息内容,时间范围,节点以及话题来设置。

对于大型项目日志查看和分析很有用了。

RViz官方用户手册

小技巧

.sh脚本一次性启动多个程序(对应多个终端)

经常开发ros环境的应该知道,当需要运行的节点太多太乱的时候,不如写个脚本直接启动多个节点(对应多个终端)。

基础知识

先介绍一下gnome-terminal命令的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gnome-terminal  # 打开一个新的终端
# 参数
# 基本用法
--maximize # 打开后自动最大化
--full-screen # 打开后全屏
--window # 打开多个终端,多个标签页
gnome-terminal --window --window #打开两个
gnome-terminal --window --tab --window --tab --tab #打开两个,第一个两个tab,第二个3个tab
-t # 设置终端的标题。注意,有些版本不支持。
# 设置打开的位置和大小(宽度x高度+左侧偏移量+上方偏移量)
gnome-terminal --geometry=80x25+10+10

# 启动后自动执行命令
-e # 可以出现多次。如果在所有--window前面,表示对所有window和tab起作用,如果在--window或者--tab后面,表示只针对这个tab执行,要注意-e后面只能有一个参数,也就是说如果有空格,需要用引号。
-x # 只能出现一次,在-x后面的所有内容,均认为是要执行的命令,所以可以出现空格,这些命令是针对所有tab都执行的。
gnome-terminal -x bash -c "ls"
gnome-terminal -e 'bash -c "ls"'
# 注,运行时会警告:
# 参数“-x”弃用并可能在 gnome-terminal 的后续版本中移除。
# 参数“-e”弃用并可能在 gnome-terminal 的后续版本中移除
# 使用“-- ”以结束选项并将要执行的命令行追加至其后。
# 所以,现在可以直接用“--”来替换掉“-x”和“-e”了。

# 执行完成后保持终端不自动关闭。
# 1. 最后加上exec bash的命令,并用分号跟前面的命令隔开。
gnome-terminal -x bash -c "ls; exec bash"
gnome-terminal -e 'bash -c "ls; exec bash"'
# 2. 修改terminal的配置,在terminal点右键,选择Profiles->Profile Preferences,然后找到Title and Command,里面有一项When command exits,后面选择为Hold the terminal open,然后就可以了。
# 3. 把结果重定向给less,这样less执行完之前,是不会退出的。
gnome-terminal -x ls|less

基本使用

了解上面后我们可以创建属于自己的脚本了:

  1. 在ROS工作空间(例如,catkin_ws文件夹)下创建一个脚本文件,例如runROS.sh

  2. 写入内容,例如:

    1
    2
    3
    4
    5
    6
    #!/bin/bash
    gnome-terminal --window -- bash -c "source ./devel/setup.bash && roslaunch PACKAGE_NAME *.launch; exec bash"
    sleep 3s
    gnome-terminal --window -- bash -c "source ./devel/setup.bash && rosrun PACKAGE_NAME NODE_NAME *.yaml; exec bash"
    sleep 3s
    gnome-terminal --window -- bash -c "rosbag play /yourfolder/*.bag; exec bash"

    需要什么节点按照这个模块添加即可。

  3. 给脚本文件授予权限:

    1
    sudo chmod 755 runROS.sh

    chmod 755 设置用户的权限为:

    1. 文件所有者可读可写可执行。
    2. 与文件所有者同属一个用户组的其他用户可读可执行。
    3. 其它用户组可读可执行。
  4. 运行脚本文件:

    1
    2
    cd catkin_ws
    ./runROS.sh
  5. 完成。

进阶使用

打开多标签页而不是多终端

修改runROS.sh文件内容为:

1
2
3
4
#!/bin/bash
gnome-terminal --window -e "bash -c 'source ./devel/setup.bash && roslaunch PACKAGE_NAME *.launch; exec bash'" \
--tab -e "bash -c 'sleep 3s; source ./devel/setup.bash && rosrun PACKAGE_NAME NODE_NAME *.yaml; exec bash'" \
-tab -e "bash -c 'sleep 6s; rosbag play /yourfolder/*.bag; exec bash'"

--经测试好像不支持--tab,脚本执行没有按照预期来,这里还是要使用-x-e

命令行使用\实现换行,方便排版。

Terminator终端和zsh shell

修改runROS.sh文件内容为:

1
2
3
4
5
6
#!/bin/zsh
terminator -e "zsh -c 'source ./devel/setup.zsh && roslaunch PACKAGE_NAME *.launch; exec zsh'"
sleep 3s
terminator -e "zsh -c 'source ./devel/setup.zsh && rosrun PACKAGE_NAME NODE_NAME *.yaml; exec zsh'"
sleep 3s
terminator -e "zsh -c 'source ./devel/setup.zsh && rosbag play /yourfolder/*.bag; exec zsh'"

这里播放rosbag前面需要加上source ./devel/setup.zsh,否则会报错zsh:1: command not found: rosbag

如果exec zsh命令未能在命令运行完成后保持终端窗口打开,可以尝试修改terminal的配置,在terminal点右键,选择Profiles->Profile Preferences,然后找到Title and Command,里面有一项When command exits,后面选择为Hold the terminal open。

因为roscoreroslanuch打开ROS节点后不会自行关闭,需要使用快捷键Ctrl+C手动关闭,所以不会运行到exec zsh这一步。

而且,使用快捷键Ctrl+C手动关闭ROS节点后,终端会直接退出,也就是关闭窗口(这个BUG?反而能自动关闭终端窗口,减少了自己的操作?)。

如果Ctrl+C后不想关闭终端,可以在terminal点右键,选择Profiles->Profile Preferences,然后找到Title and Command,里面有一项When command exits,后面选择为Hold the terminal open。不过即使这样,当前终端也不再能输入命令,只能使用Terminator的功能:水平分割/数值分割来另起一个终端。

提高脚本的扩展性

Shell脚本语法教程

主要解决播放不同rosbag包的需求,runROS.sh文件的内容修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/zsh

case "$1" in
1) bag_name="dynamic_6dof"
;;
2) bag_name="boxes_6dof"
;;
*) bag_name="dynamic_6dof"
;;
esac

echo "${bag_name}"

terminator -e "zsh -c 'source ./devel/setup.zsh && roslaunch vins vins_rviz.launch; exec zsh'"
sleep 3s
terminator -e "zsh -c 'source ./devel/setup.zsh && rosrun vins vins_node ./src/VINS-Fusion/config/rpg_davis/rpg_davis_VINS.yaml; exec zsh'"
sleep 3s
terminator -e "zsh -c 'source ./devel/setup.zsh && rosbag play -r 0.5 /Dataset/rpg_davis/${bag_name}/${bag_name}.bag; exec zsh'"

对程序简单地说明:

  • 当执行命令./runROS.sh 1时,会播放dynamic_6dof.bag
  • 当执行命令./runROS.sh 2时,会播放boxes_6dof.bag

以此类推,可以只编写使用一个脚本文件,通过输入不同的(简单的)参数,来实现运行不同的配置文件。

Terminator终端实现多标签页

以下是gpt-4的回答:

要在 Terminator 终端中实现您的需求,我们依旧会利用一个 shell 脚本。不过,值得注意的是,Terminator 的命令行界面(CLI)可能没有提供直接的方式来新建一个标签页并在其中运行特定命令。但我们仍然可以通过一些创造性的方法来尝试实现这一需求,比如使用 Terminator 的布局功能或发送按键事件来模拟用户操作。下面提供一个基于布局功能的基础脚本示例,并解释如果可能的话如何利用它来达到目的。

使用 Terminator 的布局功能

首先,你需要创建一个 Terminator 配置,其中预定义了一个具有两个标签页的布局。然而,Terminator 配置通常不允许直接在创建标签页时指定要运行的命令。作为替代,我们可以创建一个脚本,一开始就与预期的标签页中要运行的命令相对应,然后将这些脚本设置为在 Terminator 布局中的对应标签页自动运行。

下面是一个简化的例子,假设你已经有了一个包括两个标签页的 Terminator 布局(我们这里称之为 mylayout),以及如何编写一个 shell 脚本来间接实现你的需求:

步骤 1: 创建脚本来运行你的命令

run_first_command.sh

1
2
3
#!/bin/bash
# 使用 zsh 运行你的 ROS 命令
zsh -c "source ./devel/setup.zsh && roslaunch PACKAGE_NAME *.launch"

run_second_command.sh

1
2
3
4
5
#!/bin/bash
# 稍等3秒
sleep 3
# 然后运行第二个 ROS 命令
zsh -c "source ./devel/setup.zsh && rosbag play /yourfolder/*.bag"

为这两个脚本文件设置执行权限:

1
chmod +x run_first_command.sh run_second_command.sh

步骤 2: 配置 Terminator 布局

你需要手动编辑 Terminator 的配置文件,通常位于 ~/.config/terminator/config(如果还没有此文件,首次运行 Terminator 会创建)。你要编辑这个文件,加入一个自定义布局,其配置两个终端分别运行这两个脚本。请参考 Terminator 的官方文档 来学习如何配置布局。

步骤 3: 编写 shell 脚本启动 Terminator

假设你已经按照步骤 2 创建好了名为 mylayout 的 Terminator 布局,接下来,你可以编写一个简单的 shell 脚本来启动这个布局:

1
2
#!/bin/bash
terminator -l mylayout

这其中,-l 参数指定了要启动的布局名称。

总结:这个解决方案需要几个步骤和一些手动配置,而且不如在原生的 shell 脚本中那样直接和灵活。不过,它提供了一种可行的方式来实现你的需求——在 Terminator 中启动一个布局,该布局包含了运行特定命令的多个标签页。根据你的 Terminator 版本和配置情况,详细步骤可能会有所不同,因此建议查阅最新的 Terminator 文档以获得最准确的指导。

发送按键事件来模拟用户操作

要通过发送按键事件来模拟用户操作,实现在 Terminator 终端新建一个标签页并在其中运行命令的需求,你可以使用 xdotool。xdotool 是一个模拟键盘输入和鼠标活动的工具,可以用来实现自动化的交互操作。请注意,这种方法依赖于具体的窗口管理器和终端行为,可能需要根据你的系统环境微调。

首先,你需要安装 xdotool(如果尚未安装):

1
sudo apt-get update && sudo apt-get install xdotool

接下来,你可以编写一个脚本来完成这个任务。这个脚本将做以下几件事:

打开一个 Terminator 窗口。等待一小段时间,确保 Terminator 启动完成。使用 xdotool 发送按键事件来在 Terminator 中新建一个标签页。在新标签页中执行你的命令。

下面是这样一个脚本的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/bin/bash

# 启动 Terminator
terminator &

# 获取 Terminator 窗口的窗口ID,这里假设 Terminator 是最近打开的窗口
# 请根据您的系统和环境调整 sleep 时间和可能的窗口搜索逻辑
sleep 2
window_id=$(xdotool seros:time::now()详解arch --onlyvisible --class terminator | head -1)

# 确保找到了 Terminator 窗口
if [ -z "$window_id" ]; then
echo "Terminator window not found."
exit 1
fi

# 将焦点移至 Terminator 窗口
xdotool windowactivate --sync $window_id

# 新建一个标签页
# 请根据您的 Terminator 快捷键设置调整这里的键位组合
# 例如,默认使用 Ctrl+Shift+T 来新建标签页
xdotool key --clearmodifiers ctrl+shift+t

# 为了确保标签页创建成功,可能需要短暂等待
sleep 1

# 在新标签页中执行命令
# 注意:Terminator 默认可能不会直接执行通过 xdotool 发送的命令字符串
# 你可能需要通过一个临时脚本来实现自动执行命令
# 在 HOME 目录下创建临时脚本执行第一个命令
echo "source ./devel/setup.zsh && roslaunch PACKAGE_NAME *.launch" > ~/temp_cmd_1.sh
chmod +x ~/temp_cmd_1.sh
xdotool type --clearmodifiers "$HOME/temp_cmd_1.sh"
xdotool key Return

# 等待一段时间,然后再次创建新标签页并执行第二个命令
sleep 3
xdotool key --clearmodifiers ctrl+shift+t
sleep 1
echo "source ./devel/sros:time::now()详解etup.zsh && rosbag play /yourfolder/*.bag" > ~/temp_cmd_2.sh
chmod +x ~/temp_cmd_2.sh
xdotool type --clearmodifiers "$HOME/temp_cmd_2.sh"
xdotool key Return

# 清理创建的临时脚本文件
sleep 5 # 确保命令有足够的时间开始执行
rm ~/temp_cmd_1.sh ~/temp_cmd_2.sh

注意: 这个示例脚本包含许多简化的操作,真实环境下的表现可能会因系统配置不同而有所差异。请务必根据自己的终端快捷键和系统行为进行相应的调整。使用 xdotool 发送命令字符串到终端可能有一些限制,特别是对于复杂的命令。在某些情况下,使用脚本文件是绕过这些限制的一种方法。

多个工作空间同时工作

ROS开发中,有时需要建多个工作空间,将不同的功能包区别开来。但是,多个工作空间会存在相互影响的情况,如何让它们能同时工作且相互不影响?单纯改环境变量是不行的。

假设有两个工作空间/home/user1/work1_ws/home/user1/work2_ws,怎么做呢?

需要在同一个shell窗口下依次执行:

  1. work1_ws中执行catkin_make,编译完成后,执行:

    1
    source /home/user1/work1_ws/devel/setup.bash
  2. work2_ws中执行catkin_make,编译完成后,执行:

    1
    source /home/user1/work2_ws/devel/setup.bash
  3. 完成后,查看:

    1
    2
    echo $ROS_PACKAGE_PATH
    # /home/user1/work2_ws /src:/home/user1/work1_ws /src:/opt/ros/kinetic/share

    两个工作空间可以同时工作了。

  4. 核心点就是在catkin_make时确保另一个工作空间的setup.bash已经执行。

将ROS的功能包打包为.deb文件并安装

将ROS的功能包打包为deb文件,并且成功安装

ROS实验笔记之——打包roslaunch文件为deb包

ROS实验笔记之——多个工作空间共存

查看bag包每个主题的大小

新建rosbag_size_by_topic.py文件并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
# Prints total cumulative serialized msg size in bytes per topic
# Calculation takes approximately 1sec/GB
# Usage: python rosbag_size_by_topic.py BAG_FILE_PATH

import rosbag
import sys

topic_size_dict = {}
for topic, msg, time in rosbag.Bag(sys.argv[1], "r").read_messages(raw=True):
topic_size_dict[topic] = topic_size_dict.get(topic, 0) + len(msg[1])

topic_size = list(topic_size_dict.items())
topic_size.sort(key=lambda x: x[1], reverse=False)

for topic, size in topic_size:
if size < 1000:
print("{:7d}B {}".format(size, topic))
elif size >= 1000000000000:
print("{:7.2f}T {}".format(size/1000000000000, topic))
elif size >= 1000000000:
print("{:7.2f}G {}".format(size/1000000000, topic))
elif size >= 1000000:
print("{:7.2f}M {}".format(size/1000000, topic))
elif size >= 1000:
print("{:7.2f}K {}".format(size/1000, topic))

运行:

1
2
python rosbag_size_by_topic.py BAG_FILE_PATH  # or
rosrun PACKAGE rosbag_size_by_topic.py BAG_FILE_PATH

示例输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    86B  /tf_static
775B /move_base/global_costmap/inflater_layer/parameter_descriptions
775B /move_base/local_costmap/inflater_layer/parameter_descriptions
1.01K /mobile_base_controller/parameter_descriptions
1.33K /move_base/goal
1.88K /voxel_grid/parameter_descriptions
866.76K /move_base/GlobalPlanner/plan
921.19K /path_planner/visualization_marker
1.10M /move_base/TebLocalPlannerROS/local_plan
1.18M /imu/mag
1.19M /move_base/TebLocalPlannerROS/teb_markers
19.89M /tf
284.01M /move_base/GlobalPlanner/potential
19.54G /move_base/global_costmap/obstacles_layer/voxel_grid

查看bag包的开始和结束时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
import rosbag
import argparse

def print_rosbag_time(directory):
for filename in os.listdir(directory):
if filename.endswith(".bag"): # 检查文件是否为rosbag文件
filepath = os.path.join(directory, filename)
bag = rosbag.Bag(filepath)
start_time = bag.get_start_time()
end_time = bag.get_end_time()
print(f"Rosbag {filename} start time: {start_time}, end time: {end_time}")

if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Print start and end time of all rosbags in a directory.')
parser.add_argument('directory', type=str, help='The directory to search for rosbag files.')
args = parser.parse_args()

print_rosbag_time(args.directory)

自定义消息类型

  1. 新建工作区:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cd youfolder/
    mkdir -p catkin_ws/src # catkin_ws可改名

    cd catkin_ws
    catkin config --init --mkdirs --extend /opt/ros/melodic --merge-devel --cmake-args -DCMAKE_BUILD_TYPE=Release

    cd src
    catkin_init_workspace # 创建顶层CMakeLists.txt文件,CLion利用此文件打开项目

    cd ..
    catkin build # 编译生成完整工作区

    此时,./catkin_ws文件夹下有builddevellogssrc四个文件夹。

  2. 克隆catkin_simple:

    1
    2
    cd youfolder/catkin_ws/src
    git clone https://github.com/catkin/catkin_simple.git
  3. 创建消息类型:

    1
    2
    3
    4
    cd youfolder/catkin_ws/src
    mkdir detection_msgs/msg
    cd detection_msgs/msg
    touch Detection.msg

    Detection.msg中写入:

    1
    2
    3
    4
    5
    6
    float64 x
    float64 y
    float64 w
    float64 h
    int32 class_id
    float64 class_confidence
  4. 创建package.xml

    1
    2
    cd youfolder/catkin_ws/src/detection_msgs
    touch package.xml

    package.xml中写入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <?xml version="1.0"?>
    <package>
    <name>detection_msgs</name>
    <version>0.0.0</version>
    <description>The detection_msgs package</description>
    <maintainer email="zeyu.long0121@gmail.com">Zeyu Long</maintainer>
    <license>MIT</license>

    <buildtool_depend>catkin</buildtool_depend>
    <buildtool_depend>catkin_simple</buildtool_depend>

    <build_depend>std_msgs</build_depend>
    <build_depend>message_generation</build_depend>
    <build_depend>roscpp</build_depend>
    <build_depend>rosmsg</build_depend>

    <run_depend>roscpp</run_depend>
    <run_depend>rosmsg</run_depend>
    <run_depend>message_runtime</run_depend>
    <run_depend>std_msgs</run_depend>
    <run_depend>rosbag</run_depend>
    </package>
  5. 创建CMakeLists.txt

    1
    2
    cd youfolder/catkin_ws/src/detection_msgs
    touch CMakeLists.txt

    CMakeLists.txt中写入:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    cmake_minimum_required(VERSION 2.8.3)
    project(detection_msgs)

    # search for everything we need to build the messages, dont forget the message_generation
    find_package(catkin REQUIRED COMPONENTS
    message_generation
    std_msgs
    roscpp
    rospy
    )

    # search for all msg files
    FILE(GLOB messages_to_build RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/msg"
    "${CMAKE_CURRENT_SOURCE_DIR}/msg/*.msg")

    # notify catkin to look at the previously found msg files
    add_message_files(
    FILES
    ${messages_to_build}
    )

    # build the header files from the msg files, and notify catkin about the dependencies
    generate_messages(
    DEPENDENCIES
    std_msgs
    )

    # export the dependencis of this package for who ever depends on us
    catkin_package(
    CATKIN_DEPENDS message_runtime std_msgs roscpp rosmsg
    )

    include_directories(
    ${catkin_INCLUDE_DIRS}
    )

  6. 编译:

    1
    2
    cd youfolder/catkin_ws
    catkin build
  7. 编译完成后,

    • youfolder/catkin_ws/devel/include中生成了detection_msgs文件夹,把该文件夹放入你的C++项目中,引用头文件即可使用。

      1
      2
      #include <ros/ros.h>
      #include "detection_msgs/Detection.h"
    • youfolder/catkin_ws/devel/lib/python2.7/dist-packages中生成了detection_msgs文件夹,把该文件夹放入你的Python项目中,导入包,即可引用:

      1
      2
      3
      4
      5
      import rosbag
      import rospy
      from std_msgs.msg import String
      from std_msgs.msg import Header
      from detection_msgs.msg import Detection
  8. 完成。

附:DetectionArray.msg:

1
2
3
4
5
# This message contains an array of detections
Header header

# an array of detections
Detection[] detections

修改生成的DetectionArray.h:

1
2
// #include <detection_msgs/Detection.h>
#include "Detection.h"

同时播放多个rosbag

怎样同时播放多个rosbag?它们的播放顺序是怎么样的?

你可以使用rosbag play命令来播放多个rosbag。如果你在命令行中列出多个rosbag文件,它们将按照你列出的顺序播放。例如:

1
rosbag play bag1.bag bag2.bag bag3.bag

这将首先播放bag1.bag,然后是bag2.bag,最后是bag3.bag

然而,这并不会按照时间戳对齐这些rosbag。每个rosbag都会从它自己的开始时间开始播放,这可能会导致时间戳的不连续。如果你需要按照时间戳对齐这些rosbag,你可能需要先将它们合并为一个rosbag,然后再播放。

如果你需要同时播放多个rosbag,你可以在不同的终端窗口中运行rosbag play命令,或者使用&在后台运行它们。例如:

1
2
3
rosbag play bag1.bag &

rosbag play bag2.bag &

这将同时开始播放bag1.bagbag2.bag。但是请注意,这并不会按照时间戳对齐这些rosbag。

按照时间戳对齐,合并rosbag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import rosbag
from std_msgs.msg import Int32, String
import sys

def merge_rosbags(input_bags, output_bag):
with rosbag.Bag(output_bag, 'w') as outbag:
for input_bag in input_bags:
for topic, msg, t in rosbag.Bag(input_bag).read_messages():
outbag.write(topic, msg, t)

if __name__ == "__main__":
input_bags = sys.argv[1:-1] # 命令行的第一个参数到倒数第二个参数是输入的rosbag文件
output_bag = sys.argv[-1] # 命令行的最后一个参数是输出的rosbag文件
merge_rosbags(input_bags, output_bag)

按照时间戳对齐,同时播放多个rosbag

在ROS中,你可以使用rosbag play命令的--clock选项来发布一个/clock主题,这个主题可以用来同步多个rosbag。然而,rosbag play命令本身并不支持同时播放多个rosbag并按照时间戳对齐它们。

为了实现这个功能,你可能需要写一个脚本来读取多个rosbag,然后按照时间戳排序所有的消息,最后将排序后的消息发布到相应的主题。这可能需要使用到rosbag的Python API,以及rospy库来发布消息。

以下是一个简单的示例,展示了如何实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import rosbag
import rospy
from std_msgs.msg import Int32, String

def play_rosbags(input_bags):
pub_dict = {}
msg_list = []
for input_bag in input_bags:
for topic, msg, t in rosbag.Bag(input_bag).read_messages():
if topic not in pub_dict:
pub_dict[topic] = rospy.Publisher(topic, type(msg), queue_size=10)
msg_list.append((topic, msg, t))
msg_list.sort(key=lambda x: x[2])
for topic, msg, t in msg_list:
pub_dict[topic].publish(msg)
rospy.sleep(t - rospy.Time.now())

if __name__ == "__main__":
rospy.init_node('rosbag_player', anonymous=True)
input_bags = ['input1.bag', 'input2.bag', 'input3.bag'] # 你的输入rosbag文件列表
play_rosbags(input_bags)

这段代码首先定义了一个函数play_rosbags,它接受一个输入rosbag文件列表。然后,它打开每个输入rosbag文件,读取其中的所有消息,并将它们添加到一个列表中。然后,它按照时间戳对所有的消息进行排序,并将排序后的消息发布到相应的主题。

注意,这个函数假设所有输入rosbag文件的时间戳都是正确的,即它们都是相对于同一个时间原点的。如果这不是你的情况,你可能需要在发布每个消息之前调整它的时间戳。

运行多个launch文件

你可以在一个ROS launch文件中使用<include>标签来启动另一个launch文件。这样,你只需要使用一个roslaunch命令就可以启动两个launch文件。

以下是如何在launch文件中使用<include>标签的示例:

1
2
3
4
5
6
7
<launch>
<!-- Include another launch file -->
<include file="$(find your_package_name)/launch/your_other_launch_file.launch"/>

<!-- Other nodes -->
<!-- ... -->
</launch>

在这个例子中,我们使用<include>标签来包含另一个launch文件。file属性是要包含的launch文件的路径。你可以使用$(find your_package_name)来获取你的包的路径。

请注意,被包含的launch文件必须在同一个ROS工作空间中,或者在ROS_PACKAGE_PATH环境变量指定的路径中。

问题与解决

新建的工作空间不在当前路径下/删除工作空间

问题描述:使用catkin config --init初始化工作空间时,显示的Workspace的路径不在当前目录下,而是其上级(或以上)路径,

原因:工作空间不能嵌套。当前路径的上级(或以上)路径已经被设置为了工作空间。

解决:

1
2
3
4
5
6
7
8
cd `Workspace`显示的那个当前路径的上级(或以上)路径
rm -rf .catkin_tools
# 即可删除这个工作空间

# 另:
# https://catkin-tools.readthedocs.io/en/latest/verbs/catkin_config.html
catkin config -h # 可查看其可选参数
# 可以使用 -w 参数指定工作空间所在的路径(默认为当前路径)(解决不了上述问题)

参考链接

ros中的catkin是什么东西?

catkin build和catkin_make的区别和使用

复习笔记之——ROS(robot operating system)

ROS复习笔记之——ROS术语

ROS的目录文件结构

ROS的几个重要概念:节点、消息、主题、服务

ROS复习笔记之——名称

ROS复习笔记之——坐标变换(TF)

ROS复习笔记之——系统框架及其编程规范

【ROS进阶】一文搞懂ROS话题通信机制与消息队列

ROS:回调函数处理与回调队列

ROS 搞懂多话题回调机制以及消息队列

ros::spin()、ros::spinOnce():使用细节、区别

ROS复习笔记之——roslaunch的应用

ROS学习笔记之——一些常用命令的记录

ROS中多个工作空间同时工作

gnome-terminal使用

.sh脚本启动多个程序(对应多个终端)之ROS篇

ROS 信息打印(INFO、 DEBUG等)

ROS_INFO与ROS_INFO_STREAM

关于ROS日志ROS_INFO需要知道的一些东西

ros::Rate 设定的帧率循环进入一次就卡住

机器人操作系统ROS(十一):ROS时间戳及其与年月日时分秒毫秒格式的转换

ros:time::now()详解

Using rosbag to get size of each topic

catkin_make 只编译一个包

ros 编译如何选择某些文件或包不编译

ROS入门21讲笔记(四)自定义话题消息类型和使用


ROS知识积累
http://zeyulong.com/posts/c4541b88/
作者
龙泽雨
发布于
2024年3月5日
许可协议