有用的知识点

1.C++的编译

        在C++中,编译过程会生成多种形式的文件:预处理文件(.i/.ii)(展开宏和头文件)---> 汇编文件(.s)(转为平台相关的汇编代码)---> 目标文件(.o/.obj)(二进制机器码,未链接)---> 静态库(.a/.lib)(目标文件集合,直接嵌入程序)---> 动态库(.so/.dll/.dylib)(运行时加载的共享代码)以及可执行文件(如ELF、PE、Mach-O格式)。此外,Clang还可生成LLVM中间代码(.bc)用于跨平台优化。整个过程通过预处理器、编译器、汇编器和链接器协作完成,不同平台(Linux/Windows/macOS)的扩展名和工具略有差异,但核心流程一致。

2.hostname_ip

在 Ubuntu(以及其他类 Unix 系统)里,确实有一个本地文件用来手动设置 Hostname 和 IP 的映射,可以绕过 DNS 直接访问网站。/etc/hosts 是一个本地 DNS 表,用来手动指定 "主机名 <-> IP地址" 的映射关系。查找ip

当你在浏览器或代码里访问一个 hostname 时:

  1. 系统会先查 /etc/hosts 看有没有对应记录。

  2. 如果找到了,就直接用这个 IP 地址去连接。

  3. 如果没有,再去找 DNS 服务器解析。

3.vscode使用技巧

0.安装、卸载

vscode安装与完全卸载

1.vscode怎么使代码块自动修正整齐,格式化

        快捷键:Ctrl+Shift+I,可以自动把需要空格的地方和缩进的地方给你加上,使得你的代码看起来更整洁。(很好用!!)

2.vscode使用ctrl跳转到某函数定义说明之后,跳回原来的位置

        快捷键:Ctrl+Alt+-

3.vscode展开/折叠所有子区域

        快捷键:先Ctrl+K 后Ctrl+J 可以展开所有子区域;

                      先Ctrl+K 后Ctrl+0(零)可以折叠所有子区域。

4.vscode找不到自定义头文件

`参考这个 头文件路径添加,在文件里添加"${workspaceFolder}/include/**";

注意上面这一步在c_cpp_propertiest.json操作的作用是:告诉编译器头文件的位置,用于代码补全,控制 VS Code 智能提示、跳转、红线消除。

如果你的目录格局是下面这样的,还需要做其他配置,在tasks.json文件(这里才是告诉编译器如何编译操作的)加入一些编译的参数,如下下所示:

project
    --include
        swap.h
    --src
        swap.cpp
main.cpp
"tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++ 生成活动文件",
            "command": "/usr/bin/g++",
            "args": [
                "-fdiagnostics-color=always",
                
                // 要编译的源文件
                "-g",
                "${file}",
                "src/swap.cpp",
                
                // 告诉编译器头文件的位置
                "-I",
                "include",  
                
                // 告诉编译器要生成的可执行文件的位置
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "调试器生成的任务。"
        },

5.报错关于CMake的)(这个不影响的话就别理了)

        当提示:“[cmakefileapi-driver] 此版本的 CMake 不支持 "toolchains" 对象类型。编译器路径将通过读取 CMakeCache.txt 来确定。 ” (还可以解决找不到ros/ros.h的问题,很抽象)

        说明cmake版本低于 3.21(toolchains 类型是从 3.21 开始支持的);

        此时在CMakeLists.txt文件中添加:

set(CMAKE_C_COMPILER /usr/bin/gcc)
set(CMAKE_CXX_COMPILER /usr/bin/g++)

4. C++中的标识符命名最佳实践

0. 用于变量的蛇形命名法和用于函数的驼峰命名法

int my_variable_name;   // 蛇形命名法
int myVariableName()    // 驼峰命名法

1. 变量的标识符

        C++中的一个约定是变量名应该以小写字母开头。如果变量名是一个单词,则整个内容应该用小写字母书写。

int value; // 推荐
int Value; // 不推荐

        一个好的经验法则是使标识符的长度与它的使用范围成比例。具有平凡用途的标识符可以具有短名称(例如,i)。使用更广泛的标识符(例如,从程序中的许多不同位置调用的函数)应该具有更长、更具描述性的名称(例如,不要使用open,而是尝试使用openFileOnDisk)。

int open()            //不推荐
int openFileOnDisk()  //推荐  

2. 用户定义的类型

        以大写字母开头的标识符名称通常用于用户定义的类型(例如结构、类和枚举)

3. 变量的声明

        对于变量声明,使用注释来描述变量将用于什么,或者解释任何其他不明显的东西。例如,假设我们已经声明了一个名为numberOfChars的变量,用于存储一段文本中的字符数。文本“Hello World!”有10个、11个或12个字符吗?这取决于我们是包括空格还是标点符号。与其命名变量numberOfCharsIncludingWhitespaceAndPunctions(相当长),不如在声明行上或上面放置一个适当的注释,以帮助用户理解。

// 统计文本中的字符串长度,包含空格和标点
int numberOfChars;

注:

最佳实践

        在旧的程序中更改代码时,使用该程序的约定(即使它们不符合现代最佳实践)。编写新程序时,请使用现代最佳实践。

5. 代码中函数的理解

       函数通常应执行一个(且只能执行一个)任务。

       当函数变得太长、太复杂或难以理解时,可将其划分为多个子函数。称为重构。

6. C++中的显式命名空间限定符

      「::」 符号是一个称为范围解析运算符。:: 符号左侧表示命名空间, :: 符号右侧则是在命名空间中的符号。如果 :: 符号左侧没有提供标识符,则认为是全局命名空间。因此,使用std::cout时,是在说“使用命名空间std中的cout”。这是使用cout的最安全的方法,因为引用的cout(std名称空间中)没有歧义。

       避免在程序顶部或头文件中使用using指令(例如 “using namespace std;”)。它们违反了最初添加名称空间的目的。(以这种方式使用using指令时,定义的任何标识符都可能与std命名空间中的同名标识符冲突。更糟的是,也许标识符名称现在不会冲突,但可能会与未来的语言修订中添加到std命名空间的新标识符冲突。这就是将标准库中的所有标识符移动到std命名空间的好处!)

7.命令行指令"|"(pipe管道)的常见组合

        管道的作用是将前一个命令(| 左边的命令)的标准输出(Standard Output),作为后一个命令(| 右边的命令)的标准输入(Standard Input)。

1. 过滤和搜索(Filtering & Searching)

这是管道最核心的用途之一:从大量文本中筛选出你关心的内容。

  • grep(全局搜索正则表达式并打印)

    • ps aux | grep nginx

      • ps aux:列出所有正在运行的进程(输出很多行)。

      • grep nginx:从进程列表中只筛选出包含“nginx”的行。

      • 作用:快速检查某个进程(如Nginx)是否在运行。

    • cat /var/log/syslog | grep -i error

      • cat:读取系统日志文件。

      • grep -i error:不区分大小写地筛选出包含“error”的行。

      • 作用:在日志文件中快速查找错误信息。

  • head / tail(查看开头/结尾)

    • dmesg | tail -20

      • dmesg:显示内核环形缓冲区消息(启动信息等,通常很长)。

      • tail -20:只显示最后20行。

      • 作用:查看最近的内核消息。

    • history | grep docker | head -5

      • history:显示你执行过的所有命令历史。

      • grep docker:筛选出历史上所有包含“docker”的命令。

      • head -5:只显示最前面的5条结果。

      • 作用:快速找到最近用过的大概5条docker命令。


2. 排序、去重和统计(Sorting, Deduplication & Statistics)

对数据进行整理和分析。

  • sort(排序)和 uniq(去重)

    • cat access.log | cut -d' ' -f1 | sort | uniq -c | sort -nr

      • 这是一个非常经典的分析Web服务器访问日志的流水线,用于找出访问最频繁的IP地址

      1. cat access.log:读取访问日志。

      2. cut -d' ' -f1:用空格分割每一行,只取第一列(即IP地址)。

      3. sort:对IP地址进行排序(uniq 要求输入必须是排序后的)。

      4. uniq -c:统计每个唯一的IP地址出现的次数。

      5. sort -nr:按统计次数进行数字逆序排序(次数最多的排在最前面)。

  • wc(统计)

    • ls /usr/bin | wc -l

      • ls /usr/bin:列出 /usr/bin 目录下的所有文件(每行一个)。

      • wc -l:统计行数。

      • 作用:快速计算一个目录下有多少个文件。


3. 实时监控(Real-time Monitoring)

管道可以让静态命令变成动态监控工具。

  • tail -f(实时追踪文件末尾新增的内容)

    • tail -f /var/log/apache2/access.log | grep 404

      • tail -f:实时输出日志文件新增的内容。

      • grep 404:只显示包含HTTP状态码“404”(未找到)的请求。

      • 作用:实时监控网站是否有人访问了不存在的页面。


4. 分页浏览(Paging)

防止内容过多一屏显示不下。

  • less / more(分页器)

    • ps aux | less

      • ps aux:输出所有进程信息。

      • less:将结果用分页器打开,你可以用上下键、PageUp/PageDown键浏览,按 q 退出。

      • 作用:优雅地浏览长输出,而不会被刷屏。


5. 转换和编辑(Transformation & Editing)

  • awk / sed(强大的文本处理工具)

    • ls -lh | awk '{print $5, $9}'

      • ls -lh:以人类可读的格式列出文件(包括大小、文件名等)。

      • awk '{print $5, $9}':只打印第5列(文件大小)和第9列(文件名)。

      • 作用:自定义 ls 命令的输出格式。

总结表

管道组合示例作用描述
`commandgrep pattern`过滤,只显示包含特定模式的行
`commandtail -n`只显示结果的最后n行
`commandhead -n`只显示结果的最前n行
`commandsort`对结果进行排序
`commanduniq -c`统计重复行的次数(需先sort
`commandwc -l`统计结果的行数
`commandless`将结果用分页器打开,便于浏览
`commandawk '{...}'`使用awk重新格式化输出
`command1command2command3`多重管道,将处理结果一步步传递下去

核心思想: 每个命令只做一件小事,但通过管道 | 将它们像乐高积木一样组合起来,就能完成非常复杂的任务。你练习得越多,就越能体会到这种“组合拳”的强大威力。

8.shell 的理解

        当你在终端输入命令的时候,shell是需要在$PATH列出的目录列表中查找对应的命令执行文件然后解释给内核去执行(解释:~/.bashrc 是用户级脚本,用于定制Shell环境(包括修改 PATH),而 PATH 是一个具体的环境变量,定义了Shell查找命令的目录列表)。举个🌰:这就是为什么当下载了ROS之后每次使用需要在终端把ros的资源source一下或者添加到~/.bashrc里面,要不然你在终端输入ros的指令之后,shell在查找命令的目录列表的时候是定位不到对应的可执行程序的。

        0.环境变量$PATH的理解

  • 定义PATH 是一个环境变量,其值是一个用冒号 : 分隔的目录路径列表。

  • 作用:当你输入一个命令(如 ls, python, code)时,Shell会按照 PATH 中列出的目录顺序,依次在这些目录中查找该命令的可执行文件。如果找到就执行,如果找不到就会报错 command not found.

1.~/.bashrc中的export和source的区别

        export用于设置/导出一个环境变量,如export PATH="/opt/ros/humble/bin:$PATH"(将ROS2的可执行文件目录添加到PATH里面),用途:设置简单的、独立的环境变量;

        source用于加载并执行一个脚本文件,将其中的命令(通常包含多个export)应用到当前Shell环境中,如 source /opt/ros/humble/setup.bash这行命令会执行 /opt/ros/humble/setup.bash 这个脚本文件里的所有命令(如上面的export PATH="/opt/ros/humble/bin:$PATH"也在里边),用途:加载一个复杂的配置脚本,该脚本内部包含了大量的 export 命令、函数定义、别名设置等。手动把这些内容全部复制到 ~/.bashrc 里会非常混乱,用 source 则干净利落。

9.ros2工作空间下同级目录的理解

        "工作空间"是ROS中用于开发ROS 2的位置,是一个具有严格子目录排布的目录文件,colcon 是 ROS 2 的构建工具,它继承并改进了 ROS 1 的 catkin_make/catkin build。它的设计理念是“不污染源代码目录”,并将构建结果集中输出到指定的目录中。默认情况下,这些输出目录就创建在你的工作空间根目录下。

0.build/

        这是 CMake / colcon 执行编译过程的地方,包含了所有在编译过程中生成的临时文件和中间文件。每一个功能包在这都会有一个对应的同名子文件夹,里面包含C++源代码编译过程中生成的二进制、链接库等中间文件和python工具的临时构建文件。用处: 通常不需要手动操作或查看这个目录,除非你在进行深度调试,需要查看 CMake 或编译器的详细输出日志。当你再次运行 colcon build 时,colcon 会利用这里的文件来判断哪些部分需要重新编译(增量编译)。

1.install/

        这是 colcon build 命令的最终产出目录,也是 ROS 2 运行时所真正使用的目录,当你使用 ros2 runros2 launch 时,ROS 2 会从这个目录里查找并执行对应的节点和启动文件(借助上面shell的说明去理解,这就是为什么在使用你工作空间中的任何功能之前,必须先执行 source install/local_setup.bash。这个脚本会将 install/ 目录下的所有包添加到你的 ROS_PACKAGE_PATH 等环境变量中,使得 ROS 2 能够发现它们)。

内容:包含所有已编译的可执行文件、Python 模块、头文件、启动文件、消息/服务定义文件等,并且按照标准的 FHS(文件系统层次结构标准)组织。

install/your_package_name/lib/: 存放包的可执行文件(Python 脚本和 C++ 节点);

install/your_package_name/share/: 存放包的共享资源,如接口定义(msg/, srv/)、启动文件(launch/)、配置文件(config/)等;

install/your_package_name/include/: 存放 C++ 头文件;

install/setup.bash (或 .sh, .zsh): 最重要的脚本,用于“激活”或“注入”当前工作空间的环境。

分发与部署: 这个目录可以被直接打包并分发到其他机器上运行(前提是目标机器有兼容的系统库)

2.log/

colcon 在构建过程中会生成大量详细的日志信息,它们都被保存在这个目录里。

3.src/

所有的 ROS 2 包(package)的源代码都放在这里。每个包都是一个独立的文件夹。colcon build 命令会递归地查找 src/ 目录下的所有包并进行构建。colcon 严格遵守不修改源目录的原则,所以所有的构建产物(build/, install/)都放在与 src/ 同级的目录中。

4.src/package

功能包是ros2代码的组织单元,通过包,可以发布ROS 2工作并允许其他人轻松构建和使用它。功能包还整合了你这部分“工作”的实现需要哪些依赖项,以及程序的构建和编译规则。

10.github的镜像源

当终端使用git clone github出问题的话,可以git克隆下载GitHub项目镜像,在项目地址前加上https://gh-proxy.com/即可极速下载。

git clone https://gh-proxy.com/https://github.com/

11.launch包含launch

先上菜:

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, GroupAction, ExecuteProcess, TimerAction, LogInfo
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch.substitutions import PathJoinSubstitution
from ament_index_python.packages import get_package_share_directory
import os
from launch.event_handlers import OnProcessStart
from launch.actions import RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.events import Shutdown
from launch.event_handlers import OnShutdown
from launch.actions import EmitEvent

# ### 使用dlio
# ```bash 
# # 启动雷达驱动
# ros2 launch livox_ros_driver2 msg_MID360_launch.py
# # 启动点云转化
# ros2 launch tron_navigation livox_to_pointcloud2_launch.py 
# # 启动dlio
# ros2 launch direct_lidar_inertial_odometry dlio.launch.py rviz:=false
# # 启动点云转化
# ros2 launch pointcloud_to_laserscan pointcloud_to_laserscan_launch.py 
# # 启动 map_odom.py 节点
# python3 tools/map_odom_brocaster.py
# # 启动websocket桥接
# ros2 launch cmd_bridge cmd_vel_bridge.launch.py


# # 启动rviz2
# ros2 launch tron_navigation rviz.launch.py    # no include
# # 启动nav2
# ros2 launch tron_navigation tron_bringup.launch.py    # no include    
# ```

def generate_launch_description():
    # ======== 1. 启动雷达驱动 ========
    # livox_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('livox_ros_driver2'),
    #             'launch',
    #             'msg_MID360_launch.py'
    #         ])
    #         ),
    #     )
    livox_launch = ExecuteProcess(
        cmd=['ros2', 'launch', 'livox_ros_driver2', 'msg_MID360_launch.py'],
        output='screen')

    # ======== 2. 启动点云转换 ========
    # livox_pointcloud2_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('tron_navigation'),
    #             'launch',
    #             'livox_to_pointcloud2_launch.py'
    #         ])
    #     ),
    # )
    livox_pointcloud2_launch = ExecuteProcess(
        cmd=['ros2', 'launch', 'tron_navigation', 'livox_to_pointcloud2_launch.py'],
        output='screen')

    # ======== 3. 启动dlio ========
    # dlio_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('direct_lidar_inertial_odometry'),
    #             'launch',
    #             'dlio.launch.py'
    #         ])
    #     ),
    #     launch_arguments={'rviz': 'false'}.items(),
    # )
    dlio_launch = ExecuteProcess(
        cmd=['ros2', 'launch', 'direct_lidar_inertial_odometry', 'dlio.launch.py', 'rviz:=false'],
        output='screen')

    # ======== 4. 启动点云转激光 ========
    # pcl_to_laserscan_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('pointcloud_to_laserscan'),
    #             'launch',
    #             'pointcloud_to_laserscan_launch.py'
    #         ])
    #     ),
    # )
    pcl_to_laserscan_launch = ExecuteProcess(
        cmd=['ros2', 'launch', 'pointcloud_to_laserscan', 'pointcloud_to_laserscan_launch.py'],
        output='screen')

    # ======== 5. 启动 map_odom.py 节点 ========
    map_odom_process = ExecuteProcess(
        cmd=['python3', 'tools/map_odom_brocaster.py'],
        output='screen'
    )

    # ======== 6. 启动 websocket 桥接 ========
    # websocket_bridge = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('cmd_bridge'),
    #             'launch',
    #             'cmd_vel_bridge.launch.py'
    #         ])
    #     ),
    # )
    websocket_bridge = ExecuteProcess(
        cmd=['ros2', 'launch', 'cmd_bridge', 'cmd_vel_bridge.launch.py'],
        output='screen')

    '''
    # # ======== 4. 启动 RViz ========
    # rviz_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('tron_navigation'),
    #             'launch',
    #             'rviz.launch.py'
    #         ])
    #     ),
    # )

    # ======== 5. 启动 Nav2 ========
    # nav2_launch = IncludeLaunchDescription(
    #     PythonLaunchDescriptionSource(
    #         PathJoinSubstitution([
    #             get_package_share_directory('tron_navigation'),
    #             'launch',
    #             'tron_bringup.launch.py'
    #         ])
    #     ),
    # )

    # ======== 延时与启动顺序控制(可选)========
    # 先启动雷达 → 定位 → 转换 → rviz → nav2
    '''

    even_handler = [
        LogInfo(msg="Starting LiDAR..."),
        livox_launch,
        RegisterEventHandler(
            OnProcessStart(
                target_action=livox_launch,
                on_start=[
                    LogInfo(msg="Starting livox_pointcloud2_launch..."),
                    livox_pointcloud2_launch
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessStart(
                target_action=livox_pointcloud2_launch,
                on_start=[
                    LogInfo(msg="Starting dlio_launch..."),
                    dlio_launch
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessStart(
                target_action=dlio_launch,
                on_start=[
                    LogInfo(msg="Starting pcl_to_laserscan_launch..."),
                    pcl_to_laserscan_launch
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessStart(
                target_action=pcl_to_laserscan_launch,
                on_start=[
                    LogInfo(msg="Starting map_odom_process..."),
                    map_odom_process
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessStart(
                target_action=map_odom_process,
                on_start=[
                    LogInfo(msg="Starting websocket_bridge..."),
                    websocket_bridge
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessExit(
                target_action=livox_launch,
                on_exit=[
                    LogInfo(msg="LiDAR process has exited."),
                    EmitEvent(event=Shutdown(reason="LiDAR process exited") )
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessExit(
                target_action=livox_pointcloud2_launch,
                on_exit=[
                    LogInfo(msg="livox_pointcloud2 process has exited."),
                    EmitEvent(event=Shutdown(reason="livox_pointcloud2 process exited") )
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessExit(
                target_action=dlio_launch,
                on_exit=[
                    LogInfo(msg="dlio_launch process has exited."),
                    EmitEvent(event=Shutdown(reason="dlio_launch process exited") )
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessExit(
                target_action=pcl_to_laserscan_launch,
                on_exit=[
                    LogInfo(msg="pcl_to_laserscan_launch process has exited."),
                    EmitEvent(event=Shutdown(reason="pcl_to_laserscan_launch process exited") )
                ]
            )
        ),
        RegisterEventHandler(
            OnShutdown(
                on_shutdown=[
                    LogInfo(msg="Launch was asked to shut down, cleaning up...")
                ]
            )
        )

    ]

    return LaunchDescription(even_handler)

有两个版本,第一个版本是用launchIncludedescription来包含launch文件,但是EventHandler不能处理launch文件,只能采用第二个版本Executeprocess把launch文件当成一个进程来用才可以,不过这样不利于Launch文件内部的管理。

原因:

        事件处理器(如OnProcessStartOnProcessExit)需要明确的目标动作来绑定。

  • ExecuteProcess: 创建一个明确的进程动作,launch系统可以直接跟踪和管理这个进程

  • IncludeLaunchDescription: 引入另一个launch文件,该文件可能包含多个动作和节点,launch系统将其视为一个"黑盒"。

   IncludeLaunchDescription启动的进程被封装在子launch上下文中,父launch系统无法直接访问这些进程的详细信息。

12. URDF文件理解

0. visual和collision的mesh文件差异

<visual>常用.dae文件,纹理材质视觉效果更好;

<collision>常用.stl文件,属于简化的几何模型,更便于计算;

13. 里程计的理解

里程计并不是移动底盘的物理组件,里程计是一个计算值/估计值;

🔧 物理组件(硬件)

  • 轮式编码器:测量车轮转动角度的传感器

  • IMU:测量加速度和角速度的芯片

  • GPS:接收卫星信号的模块

  • 摄像头:视觉传感器

🧮 计算值(软件/算法)

  • 里程计:基于传感器数据计算得出的位姿估计

    • "我从起点移动了多远?"

    • "我转了多少角度?"

    • "我现在的速度和角速度是多少?"

物理世界
    ↓
[硬件传感器] ←── 这些是物理组件
    ↓ 产生原始数据
轮编码器脉冲 | IMU读数 | 摄像头图像
    ↓
[算法/节点] ←── 这些计算里程计
    ↓ 计算得出里程计值
/wheel_odom | /imu_odom | /visual_odom
    ↓
[robot_localization] ←── 融合算法
    ↓ 输出最佳估计(提供了一个单一、可靠、权威的里程计源,所有其他节点(如move_base、rviz2等)都应该使用这个融合后的结果)
/odometry/filtered 和 odom→base_link TF

        通常的机器人设置至少包括轮编码器和惯性测量单元 (IMU) 作为其里程计传感器来源。当``robot_localization``接收到多个传感器提供的里程计信息时,它可以通过使用状态估计节点来融合传感器提供的里程计信息。这些节点使用扩展卡尔曼滤波器(ekf_node)或无迹卡尔曼滤波器(ukf_node)来实现融合。如果在配置中启用了``robot_localization``软件包,它将通过``odometry/filtered``和``accel/filtered``主题发布融合的传感器数据。此外,它还可以在``/tf``主题上发布``odom`` => ``base_link``的变换。

14. dlio/fastlio2与slam与nav2

问题:dlio和fastlio2的作用是什么,我看这两个也都能实现定位和建图,他们和slam的关系是什么,然后我看在完整的项目里面,一般是先启动dlio或者fastlio2之后再启动navigation的相关包,这之间的联系又是什么,详细解释一下。

回答:

好——我把核心概念、二者在系统里的职责、它们跟“SLAM”的关系、以及为什么常常是先启动 DLIO / FAST-LIO2(LIO) 然后再启动 navigation(导航)包,做一个系统而详细的说明,并给出实际启动/集成建议(包含常见 topic / TF / 服务 / 顺序)。我尽量贴合你在 Tron1 轮足移动机器人上的实际场景(传感器:LiDAR、IMU、可能还有里程计/相机/轮速),直接可用。

先给结论性一句话:
DLIO / FASTLIO2 都是「LiDAR-Inertial Odometry(LIO)」模块 —— 实时把传感器数据融合成连续的位姿(里程计)并输出局部地图;而 SLAM 是一个更广义的流程/系统,除了里程计外还包含回环检测、位姿图优化、全局地图输出与长期一致性。导航(Nav)使用 LIO/SLAM 的输出(位姿和地图)来做定位、代价地图、规划与控制。

下面分要点展开。


1) DLIO / FAST-LIO2 到底做什么?(职责、输出)

两者本质上都是 LiDAR + IMU → 实时里程计 / 局部地图 的系统,但实现细节、设计目标略有不同:

  • 主要目标

    • 在实时条件下把 LiDAR 点云和 IMU(以及有时轮速/里程计)融合,估计机器人在连续时间上的位姿(6DoF),并对 LiDAR 的运动畸变做校正(deskew)。

    • 输出稳定的 odometry(里程计)、发布 TF(通常 odom->base_link)和局部点云/局部子图(供后端或可视化),并可能提供保存地图的接口。

  • 具体输出(典型)

    • 一个 nav_msgs/Odometry 或自定义 odom topic(频率高、实时)。

    • tf:发布 base_link 相对于 odom(或直接 odom->base_link)的变换(连续更新)。

    • 局部地图或关键帧点云(sensor_msgs/PointCloud2.pcd 保存服务等)。

    • 有的实现带有轻量回环检测或重定位,但核心还是“里程计 + deskew + 局部配准”。

  • DLIO vs FASTLIO2 区别(简述)

    • DLIO(Direct LIO):论文/实现强调「连续时间轨迹建模」与“轻量级、直接的点云-IMU融合”,精于运动畸变校正,设计追求计算效率。

    • FAST-LIO2:以高效率著称的 LIO 实现(C++ 优化),实战中广泛用于高性能实时场景,通常包含良好的数据结构和速度优化。

    • 实际上功能重叠很大:两者都能做定位和局部地图构建,但在算法细节、鲁棒性与参数敏感性上有差异(不同场景表现不同)。

  • nav_msgs/Odometry

    header:
      frame_id: "map"
    pose:
      pose:
        position: {x: 1.23, y: 2.34, z: 0.45}
        orientation: {x: 0.0, y: 0.0, z: 0.707, w: 0.707}
    twist:
      twist:
        linear: {x: 0.1, y: 0.0, z: 0.0}
        angular: {x: 0.0, y: 0.0, z: 0.02}


2) SLAM(同时定位与建图)与 LIO 的关系

  • SLAM(概念):定位(Localization)+ 建图(Mapping)。完整的 SLAM 系统通常包含:

    1. 里程计模块(odometry / frontend):实时估计连续运动(LIO 就在这里)。

    2. 回环检测(loop-closure):检测回到过去位置以约束误差。

    3. 后端优化(pose-graph / PGO / 全局优化):把回环、里程计约束组合,做全局一致性优化。

    4. 地图管理 / 保存 / 查询(map server):保存成全局地图,供重定位或导航使用。

  • 因此

    • LIO = SLAM 的前端(或“实时里程计”)。它负责把传感器转换成连续位姿与局部地图。

    • 完整 SLAM = LIO 前端 + 回环检测 + 全局优化 + map server 等。很多工程里把 LIO 作为里程计前端,再在上面叠加 PGO(pose graph optimization)、HBA、回环检测等以得到全局一致地图。

  • 后果:如果只用 LIO(没有回环/优化),导航可以运行但长期会有漂移(尤其是大范围)——这就是为什么很多项目里会把 LIO(或 DLIO/FASTLIO2)和 PGO/HBA/localizer 组合起来,得到既实时又一致的地图。


3) 为什么“先启动 LIO(DLIO/FASTLIO2)再启动 navigation”?两者如何衔接

导航栈(无论 ROS1 的 move_base 还是 ROS2 的 nav2)依赖于几类实时数据:

  • 实时位姿(机器人在世界里的当前估计),通常通过 tfodom topic 提供(odomtf)。

  • 环境信息 / 障碍物:通常来自激光(/scan)或点云、深度相机的传感器数据(用来构建 costmap / obstacle layer)。

  • 地图(静态或全局):如果使用基于地图的导航(规划全局路径),需要一个 map_server 或 localization node(例如 AMCL / LiDAR localization)提供 map->odom 的 TF 或局部化结果。

因此启动顺序与原因:

  1. 传感器驱动先起(LiDAR、IMU、相机)——保证数据流入。

  2. 启动 LIO(DLIO/FASTLIO2):它去订阅点云与 IMU,发布 odomtf(odom->base_link)、局部点云或障碍信息。为什么先?因为 navigation 的 costmap、localization、planner 都需要持续、低延迟的位姿与障碍物输入。没有 LIO,导航没有参考坐标和局部地图,不能安全规划。

  3. 启动 map server / SLAM 后端(可选):如果使用全局地图或需要长期一致性,启动回环检测 / PGO / 地图保存模块,生成 map(静态地图)并发布 map->odom 或提供服务。

  4. 启动 localization(可选):例如 AMCL(基于激光)或 LiDAR 重定位模块,把机器人定位到 map 上,发布 map->odom TF。

  5. 启动 navigation(Nav2):它使用 tf 树(map->odom->base_link),costmap 会订阅激光/点云以感知道路障,planner 基于 maplocal_costmap 做路径规划并发布 cmd_vel

关键点:navigation 依赖 LIO 提供稳定的里程计(odom)与障碍感知流;如果你先启动 navigation 而 LIO 后起,导航会因为短时间没有位姿/地图而报错或进入错误状态。


4) TF / Frame(坐标系)关系:理解这一点很重要

标准坐标链通常是(ROS 推荐):

mapodombase_linksensor frames

含义:

  • base_link:机器人机体坐标。

  • odom:短期、连续、无漂移补偿的里程计坐标系(LIO 输出往往是 odom->base_link)。当里程计漂移时,odom 会累积误差。

  • map:全局一致的地图坐标系(SLAM 后端/重定位维护),用于长期导航和规划。

谁发布哪个 TF?

  • LIO(DLIO/FASTLIO2):通常发布 odom->base_link(实时),并发布 odom 下的 tf 与 /odom topic。

  • SLAM 后端 / relocalizer / map_server:负责 map->odom(当发现回环或做重定位时,会发布或调整 map->odom,以把 odom 校正到全局 map)。

  • navigation(Nav2)使用 map->odom + odom->base_link 得到 map->base_link,进而规划和控制。

如果只有 LIO 而没有 map->odom(即没有全局 SLAM/重定位),导航仍可在局部运行(基于 odom),但全局规划/重复运行会因为漂移出错。


5) 实战集成要点(你在 Tron1 上要特别注意的点)

下面给出一个清单,帮助你把 LIO(DLIO/FASTLIO2)和 navigation 整合好:

A. 保证时间同步 & 外参准确

  • LiDAR 与 IMU 必须时间同步(hardware sync 或 software approximate)。

  • LiDAR ↔ IMU 的外参(位移 + 旋转)要标定准确,误差会快速影响里程计。

B. 话题 / 数据接口(典型 names,可据你实现调整)

  • LiDAR 点云: /points_raw/velodyne_pointssensor_msgs/PointCloud2

  • IMU: /imu/data (sensor_msgs/Imu)

  • 里程计: /odom (nav_msgs/Odometry)(由 LIO 发布)

  • TF: odom -> base_link(LIO) 和 map -> odom(SLAM/PGO/Localizer)

  • 地图: /map(如果使用 map_server)

  • cmd: navigation 发布 /cmd_vel 给底层控制器

C. 启动顺序示例(基于 ROS2 / Nav2 的推荐)

  1. sensor drivers(LiDAR driver、IMU driver、wheel odom)

  2. LIO 节点(DLIO / FASTLIO2) → 发布 /tf(odom->base_link)/odom、局部点云

  3. (可选)PGO / 回环检测 / 地图优化 后端 → 生成全局 map 并发布 map->odom(或提供服务)

  4. map_server(如果使用静态 map) + localization(AMCL 或 LiDAR localizer)

  5. nav2 lifecycle bringup(planner, controller, bt_navigator 等)→ 启动 costmap(订阅 pointcloud/scan)

  6. application:send goals、monitor paths、调参

D. 如何把 LIO 的局部地图用到导航

  • costmap 的 obstacle layer 可以直接订阅 LIO 输出的点云或激光 topic(例如 /points_local -> 屏蔽动态点 -> 转入 costmap)。

  • global planner 通常使用 map(静态或 SLAM 输出)来做长路径规划。若没有 map,可做纯基于 odom 的局部避障导航(适用于短任务)。

E. 漂移与回环修正

  • 若环境会大量重复、或任务长期航行,必须加回环检测 + PGO(或周期性地把 LIO 输出的轨迹做全局优化),并把 map->odom 发布给 navigation。

  • 常见做法:LIO 做前端,PGO(GTSAM 等)做后端。后端一旦优化出新的全局位姿,就发布 map->odom 修正 odom,从而让 nav 使用修正后一致的坐标。

F. 多传感器融合建议

  • 在轮足机器人上,融合轮速里程计(wheel odom)可以提高平面运动精度(尤其在特征稀少的场景)。考虑用 EKF / robot_localization 把 LIO 与 wheel odom、IMU、视觉里程计融合(取决你需要的精度与鲁棒性)。但注意时间同步与坐标变换正确配置。


6) 常见问题与排查(实操技巧)

  • 导航突然漂移/规划失败:检查 map->odom 是否存在/合理;若没有,导航可能在使用一个漂移的 odom。

  • costmap 空或障碍识别不对:检查 LIO 是否发布点云到正确 topic,并且 costmap 配置的 topic 名和 frame(通常 base_link)一致。

  • TF 问题:用 tf2_echo / ros2 run tf2_tools view_frames(或 rosrun tf tf_monitor)检查 map->odomodom->base_link

  • CPU 占用高 / 频率低:LIO 节点实时性差会导致 navigation 卡顿,降低点云分辨率或采样率;或把重计算(PGO/HBA)放成离线/低频任务。

  • 机器人定位在地图外:检查 localization(AMCL)是否使用正确的 initial pose,或调用 relocalize 服务(如果有)。


7) 一个具体的、简明的整合示例(ROS2 / Nav2 风格 — 供参考)

(假设你有:LiDAR driver → /points_raw,IMU → /imu/data,正在用 FASTLIO2 作为 odom)

启动步骤(示例):

  1. 启动 LiDAR 与 IMU 驱动。

  2. 启动 FASTLIO2(参数指定 /points_raw/imu/data),它开始发布 /odomtf(odom->base_link)

  3. 启动 PGO / 回环后端(可选)来维护 mapmap->odom

  4. 启动 map_server(如果你已经有 map)或让 PGO 输出并加载 map。

  5. 启动 localization 节点(如果使用),它订阅 /scan/points,并发布 map->odom

  6. 启动 nav2(lifecycle bringup):planner、controller、bt-nav、costmap(costmap 的 sensor topics 指向 LIO 的局部点云或 LiDAR 原始话题)。

  7. 下发导航目标(nav2_simple_commander 或 RViz 2 发送 goal)。


8) 给你 Tron1 的具体建议(实务清单)

  1. 先在地面做低速采集,验证 LIO 在你的地形(地面反射、轮足扰动)下能稳定输出路径。

  2. 做 LiDAR-IMU 外参与时间同步标定(严格去做),IMU 噪声模型要在 cfg 中设好。

  3. 地面点滤波:地面分割(移除大量地面点)通常能提升点云配准的稳定性。

  4. 把 wheel odom 作为额外观测(若可靠),用 robot_localization 或 EKF 融合以减少漂移。

  5. 如果使用 ROS2:确认 DLIO/FASTLIO2 的 ROS1/ROS2 版本兼容性;若需要,优先用已经移植的 ROS2 分支或考虑做少量适配代码。

  6. 测试导航堆栈:从局部(小范围)到大范围逐步放开,观察回环时 map->odom 的修正效果。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值