ros学习

ros学习

1.创建项目

创建常用命令
复制代码
# 1. 创建工作空间目录(命名建议:xxx_ws,如ros2_ws)
mkdir -p ~/ros2_ws/src

# 2. 进入工作空间目录
cd ~/ros2_ws

# 3. 初始化工作空间(仅需执行1次)
colcon build


# 进入src目录(必须在src下创建功能包)
cd ~/ros2_ws/src

# 通用命令模板
ros2 pkg create <包名> --build-type <构建类型> --dependencies <依赖包> [--node-name <节点名>]

# 示例1:创建C++功能包(最常用)
ros2 pkg create my_cpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs

# 示例2:创建Python功能包
ros2 pkg create my_py_pkg --build-type ament_python --dependencies rclpy std_msgs

# 示例3:创建带默认节点的Python包(快捷方式)
ros2 pkg create my_py_node --build-type ament_python --dependencies rclpy --node-name my_first_node
编译常用命令
复制代码
# 回到工作空间根目录
cd ~/ros2_ws

# 编译所有功能包
colcon build

# 编译指定功能包(推荐,节省时间)
colcon build --packages-select my_cpp_pkg

# 编译Python包时自动安装依赖(可选)
colcon build --packages-select my_py_pkg --symlink-install
创建节点模板
my_first_node.py
复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ROS2 Python 通用节点模板
功能包含:日志输出、参数管理、话题发布/订阅、定时器、服务端/客户端、节点销毁处理
适配ROS2 Humble/Foxy/Iron版本
"""

# 导入ROS2核心库
import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter
from rclpy.executors import SingleThreadedExecutor
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup

# 导入标准消息类型(根据需求增减)
from std_msgs.msg import String, Int32
from std_srvs.srv import SetBool  # 标准服务类型(布尔值设置)

# 自定义话题消息(如需自定义,需先创建msg文件并配置)
# from my_py_pkg.msg import CustomMsg  # 示例:导入自定义消息

class MyFirstNode(Node):
    """
    自定义ROS2节点类,继承自rclpy.node.Node
    封装所有核心功能,便于维护和扩展
    """
    def __init__(self):
        # 1. 初始化节点,指定节点名称(必须唯一)
        super().__init__("my_first_node")
        self.get_logger().info("=== 自定义节点已启动 ===")

        # 2. 参数管理(声明+获取,支持动态修改)
        # 声明参数(参数名,默认值,参数类型)
        self.declare_parameter("node_name", "my_first_node", Parameter.Type.STRING)
        self.declare_parameter("publish_frequency", 1.0, Parameter.Type.DOUBLE)  # 发布频率(Hz)
        self.declare_parameter("max_count", 100, Parameter.Type.INTEGER)

        # 获取参数值(带默认值兜底,防止参数未设置)
        self.node_name = self.get_parameter("node_name").get_parameter_value().string_value
        self.pub_freq = self.get_parameter("publish_frequency").get_parameter_value().double_value
        self.max_count = self.get_parameter("max_count").get_parameter_value().integer_value

        # 打印参数信息
        self.get_logger().info(f"节点名称: {self.node_name}")
        self.get_logger().info(f"发布频率: {self.pub_freq} Hz")
        self.get_logger().info(f"最大计数: {self.max_count}")

        # 3. 初始化变量
        self.count = 0  # 计数器(用于定时器发布数据)

        # 4. 话题发布器(创建发布者)
        # 参数:话题名、消息类型、队列大小(防止消息丢失)
        self.string_pub = self.create_publisher(String, "chatter", 10)
        self.int_pub = self.create_publisher(Int32, "count_topic", 10)

        # 5. 话题订阅器(创建订阅者)
        # 参数:话题名、消息类型、回调函数、队列大小
        self.sub = self.create_subscription(
            String, 
            "input_topic", 
            self.sub_callback,  # 订阅回调函数
            10
        )

        # 6. 定时器(周期性执行任务,如发布话题)
        # 参数:定时器周期(秒)、回调函数、回调组(可选,用于并发)
        self.timer = self.create_timer(
            1.0 / self.pub_freq,  # 周期 = 1/频率
            self.timer_callback,
            callback_group=MutuallyExclusiveCallbackGroup()
        )

        # 7. 服务端(提供服务,响应客户端请求)
        self.srv_server = self.create_service(
            SetBool,  # 服务类型
            "reset_count",  # 服务名
            self.srv_callback  # 服务回调函数
        )

        # 8. 服务客户端(调用其他节点的服务,示例)
        self.srv_client = self.create_client(SetBool, "other_node_service")
        # 等待服务端上线(最多等待5秒)
        if not self.srv_client.wait_for_service(timeout_sec=5.0):
            self.get_logger().warn("未找到服务 'other_node_service',跳过客户端初始化")

    def timer_callback(self):
        """
        定时器回调函数:周期性发布话题数据
        每次执行计数器+1,达到最大值后重置
        """
        # 1. 更新计数器
        self.count += 1
        if self.count > self.max_count:
            self.count = 0
            self.get_logger().info(f"计数器已重置,当前值: {self.count}")

        # 2. 发布String类型话题
        string_msg = String()
        string_msg.data = f"Hello ROS2! Count: {self.count}"
        self.string_pub.publish(string_msg)

        # 3. 发布Int32类型话题
        int_msg = Int32()
        int_msg.data = self.count
        self.int_pub.publish(int_msg)

        # 4. 日志输出(不同级别:debug/info/warn/error/fatal)
        self.get_logger().debug(f"发布数据: {string_msg.data}")  # 调试级(默认不显示,需设置日志级别)

    def sub_callback(self, msg):
        """
        话题订阅回调函数:接收订阅的话题数据并处理
        参数msg:接收到的消息对象(类型与订阅的话题一致)
        """
        self.get_logger().info(f"接收到话题数据: {msg.data}")
        # 可在此处添加数据处理逻辑(如解析、转发、控制等)

    def srv_callback(self, request, response):
        """
        服务端回调函数:处理客户端请求并返回响应
        参数:
            request: 客户端发送的请求数据
            response: 要返回给客户端的响应数据
        """
        if request.data:  # 如果请求的bool值为True
            self.count = 0  # 重置计数器
            response.success = True
            response.message = "计数器已重置为0"
            self.get_logger().info("服务请求:重置计数器成功")
        else:
            response.success = False
            response.message = "拒绝重置计数器"
            self.get_logger().warn("服务请求:拒绝重置计数器")
        return response

    def call_service_example(self):
        """
        服务客户端调用示例(可在需要时调用)
        """
        # 构造请求数据
        req = SetBool.Request()
        req.data = True  # 请求参数
        # 异步调用服务(非阻塞)
        future = self.srv_client.call_async(req)
        # 等待调用结果(阻塞,可选)
        rclpy.spin_until_future_complete(self, future)
        if future.result() is not None:
            self.get_logger().info(f"服务调用结果: {future.result().message}")
        else:
            self.get_logger().error("服务调用失败")

    def destroy_node(self):
        """
        节点销毁时的清理操作(可选,继承自Node类)
        用于释放资源、保存数据、打印结束日志等
        """
        self.get_logger().info("=== 自定义节点已关闭 ===")
        super().destroy_node()  # 必须调用父类的销毁方法

def main(args=None):
    """
    节点主函数:ROS2节点的入口
    """
    # 1. 初始化ROS2上下文
    rclpy.init(args=args)

    try:
        # 2. 创建节点实例
        node = MyFirstNode()

        # 3. 自旋节点(保持节点运行,处理回调)
        # 单线程执行器(默认)
        rclpy.spin(node)

    except KeyboardInterrupt:
        # 捕获Ctrl+C,优雅退出
        node.get_logger().info("接收到退出信号,正在关闭节点...")
    finally:
        # 4. 销毁节点,关闭ROS2上下文
        node.destroy_node()
        rclpy.shutdown()

# 程序入口(ROS2节点标准写法)
if __name__ == "__main__":
    main()
setup.py
复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ROS2 Python 功能包通用 setup.py 模板
支持:多节点配置、自定义msg/srv/action、数据文件(launch/rviz/config)部署、依赖管理、测试配置
"""

from setuptools import setup, find_packages
import os
from glob import glob

# ===================== 核心配置常量(需根据实际需求修改) =====================
PACKAGE_NAME = 'my_py_node'          # 功能包名称(必须与package.xml一致)
VERSION = '0.1.0'                    # 版本号(自定义,如0.1.0、1.0.0)
MAINTAINER = 'jzx'                   # 维护者姓名
MAINTAINER_EMAIL = 'jzx@todo.todo'   # 维护者邮箱
DESCRIPTION = 'ROS2 Python通用功能包模板,支持多节点、自定义消息/服务、数据文件部署'  # 功能包描述
LICENSE = 'Apache-2.0'               # 开源协议(常用:Apache-2.0、MIT、BSD)
PYTHON_REQUIRES = '>=3.8'            # Python版本要求(Humble对应3.8,Foxy对应3.8,Iron对应3.10)

# ===================== 自动推导配置(无需手动修改) =====================
# 1. 自动查找所有launch文件(用于部署到share目录)
launch_files = glob(os.path.join('launch', '*.launch.py'))
# 2. 自动查找所有配置文件(rviz、yaml等,用于部署到share目录)
config_files = glob(os.path.join('config', '*.yaml')) + glob(os.path.join('config', '*.rviz'))
# 3. 自动查找所有msg/srv/action文件(用于声明编译依赖)
msg_files = glob(os.path.join(PACKAGE_NAME, 'msg', '*.msg'))
srv_files = glob(os.path.join(PACKAGE_NAME, 'srv', '*.srv'))
action_files = glob(os.path.join(PACKAGE_NAME, 'action', '*.action'))

# ===================== 核心setup配置 =====================
setup(
    # 1. 基础信息(必须与package.xml一致)
    name=PACKAGE_NAME,
    version=VERSION,
    packages=find_packages(exclude=['test', 'docs']),  # 排除测试/文档目录
    data_files=[
        # 2. ROS2包索引文件(必须配置,否则ros2 pkg list找不到包)
        ('share/ament_index/resource_index/packages',
            ['resource/' + PACKAGE_NAME]),
        ('share/' + PACKAGE_NAME, ['package.xml']),
        
        # 3. 部署launch文件(自动查找,无需手动添加)
        ('share/' + PACKAGE_NAME + '/launch', launch_files),
        
        # 4. 部署配置文件(rviz/yaml等,自动查找)
        ('share/' + PACKAGE_NAME + '/config', config_files),
        
        # 5. 部署自定义msg/srv/action文件(供其他包依赖)
        ('share/' + PACKAGE_NAME + '/msg', [f for f in msg_files if os.path.exists(f)]),
        ('share/' + PACKAGE_NAME + '/srv', [f for f in srv_files if os.path.exists(f)]),
        ('share/' + PACKAGE_NAME + '/action', [f for f in action_files if os.path.exists(f)]),
        
        # 6. 可选:部署其他静态资源(如模型、地图、脚本等)
        # ('share/' + PACKAGE_NAME + '/models', glob('models/*')),
        # ('share/' + PACKAGE_NAME + '/maps', glob('maps/*.pgm') + glob('maps/*.yaml')),
    ],
    
    # 7. 安装依赖(ROS2核心依赖+Python第三方库)
    install_requires=[
        # ROS2核心依赖
        'setuptools>=50.3.0',  # 最低版本要求
        'rclpy>=3.0.0',        # ROS2 Python核心库
        'std_msgs',            # 标准消息类型
        'sensor_msgs',         # 传感器消息(如激光、图像)
        'geometry_msgs',       # 几何消息(如位姿、速度)
        'nav_msgs',            # 导航消息(如路径、里程计)
        'action_msgs',         # 动作消息(如需使用action)
        'rosidl_runtime_py',   # 自定义msg/srv编译依赖
        
        # Python第三方库(根据需求添加)
        'numpy>=1.21.0',       # 数值计算
        'pyserial>=3.5',       # 串口通信
        'opencv-python>=4.5.0',# 图像处理
        'pytest>=7.0.0',       # 测试框架(也可放extras_require)
    ],
    
    # 8. 兼容性配置(防止zip包导致的路径问题)
    zip_safe=True,
    
    # 9. 维护者信息(必须与package.xml一致)
    maintainer=MAINTAINER,
    maintainer_email=MAINTAINER_EMAIL,
    description=DESCRIPTION,
    license=LICENSE,
    python_requires=PYTHON_REQUIRES,
    
    # 10. 可选:测试依赖(仅在运行pytest时安装)
    extras_require={
        'test': [
            'pytest',          # 单元测试框架
            'pytest-cov',      # 测试覆盖率
            'pytest-asyncio',  # 异步测试(如需)
            'flake8',          # 代码风格检查
        ],
    },
    
    # 11. 节点入口配置(核心!ROS2 run 运行节点的关键)
    # 格式:"节点执行名 = 包名.文件名:主函数名"
    entry_points={
        'console_scripts': [
            # 示例1:基础节点(对应my_first_node.py)
            'my_first_node = my_py_node.my_first_node:main',
            
            # 示例2:话题发布节点(可新增)
            'topic_publisher = my_py_node.topic_publisher:main',
            
            # 示例3:话题订阅节点(可新增)
            'topic_subscriber = my_py_node.topic_subscriber:main',
            
            # 示例4:服务端节点(可新增)
            'service_server = my_py_node.service_server:main',
            
            # 示例5:客户端节点(可新增)
            'service_client = my_py_node.service_client:main',
            
            # 示例6:动作服务端/客户端(可新增)
            # 'action_server = my_py_node.action_server:main',
            # 'action_client = my_py_node.action_client:main',
        ],
    },
    
    # 12. 可选:控制台脚本元数据(非必须)
    classifiers=[
        'Intended Audience :: Developers',
        'License :: OSI Approved :: Apache Software License',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.10',
        'Operating System :: Linux',
        'Topic :: Robotics',
    ],
)
package.xml
复制代码
<?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">
  <!-- ===================== 核心基础信息(必须配置) ===================== -->
  <!-- 功能包名称:必须与setup.py中的PACKAGE_NAME一致,小写+下划线 -->
  <name>my_py_node</name>
  
  <!-- 版本号:遵循语义化版本(主版本.次版本.修订版),与setup.py一致 -->
  <version>0.1.0</version>
  
  <!-- 功能包描述:清晰说明包的用途 -->
  <description>
    ROS2 Python通用功能包模板,支持多节点、自定义消息/服务/动作、
    话题发布订阅、服务端客户端、定时器、参数管理等核心功能,
    适配机器人感知、控制、决策等场景开发。
  </description>
  
  <!-- 维护者信息:姓名+邮箱(必填,便于协作) -->
  <maintainer email="jzx@xxx.com">jzx</maintainer>
  
  <!-- 作者信息(可选,多人用多个<author>标签) -->
  <author email="dev1@xxx.com">Developer1</author>
  <author>Developer2</author>
  
  <!-- 许可证:常用Apache-2.0/MIT/BSD,与setup.py一致 -->
  <license>Apache-2.0</license>

  <!-- ===================== 依赖配置(核心!按场景分类) ===================== -->
  <!-- 1. 运行依赖:节点运行时必须的依赖(与setup.py的install_requires对应) -->
  <!-- ROS2核心依赖 -->
  <depend>rclpy</depend>                <!-- Python核心库 -->
  <depend>std_msgs</depend>             <!-- 标准消息类型(String/Int32等) -->
  <depend>sensor_msgs</depend>          <!-- 传感器消息(激光/图像/IMU等) -->
  <depend>geometry_msgs</depend>        <!-- 几何消息(位姿/速度/坐标系等) -->
  <depend>nav_msgs</depend>             <!-- 导航消息(路径/里程计/地图等) -->
  <depend>action_msgs</depend>          <!-- 动作消息(如需使用Action) -->
  <depend>rosidl_runtime_py</depend>    <!-- 自定义msg/srv编译运行依赖 -->
  
  <!-- 可选:其他常用依赖 -->
  <!-- <depend>tf2_ros</depend>           <!-- 坐标系变换 -->
  <!-- <depend>rviz2</depend>             <!-- 可视化工具 -->
  <!-- <depend>ament_index_python</depend> <!-- 索引查询 -->

  <!-- 2. 构建依赖:仅编译时需要,运行时不需要 -->
  <buildtool_depend>ament_python</buildtool_depend>  <!-- Python构建工具(必须) -->
  <build_depend>rosidl_default_generators</build_depend>  <!-- 自定义msg/srv编译生成器 -->

  <!-- 3. 执行依赖:编译+运行都需要(部分场景替代depend) -->
  <exec_depend>rosidl_default_runtime</exec_depend>  <!-- 自定义msg/srv运行时依赖 -->
  <exec_depend>python3-numpy</exec_depend>           <!-- Python第三方库(系统级) -->
  <!-- <exec_depend>python3-opencv</exec_depend>        <!-- OpenCV(系统级) -->

  <!-- 4. 自定义msg/srv/action必须的组声明 -->
  <member_of_group>rosidl_interface_packages</member_of_group>

  <!-- 5. 测试依赖:仅运行测试时需要 -->
  <test_depend>ament_copyright</test_depend>  <!-- 版权声明检查 -->
  <test_depend>ament_flake8</test_depend>     <!-- 代码风格检查(PEP8) -->
  <test_depend>ament_pep257</test_depend>    <!-- 文档字符串检查 -->
  <test_depend>python3-pytest</test_depend>  <!-- Python测试框架 -->
  <test_depend>ament_lint_auto</test_depend>  <!-- 自动触发所有lint检查 -->
  <test_depend>ament_lint_common</test_depend><!-- 通用lint配置 -->

  <!-- ===================== 构建类型与扩展配置 ===================== -->
  <!-- 构建类型:必须声明,与功能包类型一致(ament_python/Python,ament_cmake/C++) -->
  <export>
    <build_type>ament_python</build_type>
    
    <!-- 可选:标记为Lint自动检查(简化测试配置) -->
    <ament_lint_auto>true</ament_lint_auto>
    <!-- 可选:排除不需要的Lint检查(如需) -->
    <!-- <ament_lint_exclude>ament_copyright</ament_lint_exclude> -->
  </export>
</package>

2.节点学习

1. 节点的本质
  • 节点(Node)是 ROS2 中最小的可执行单元,一个机器人应用由多个独立节点组成(如 "激光雷达节点""运动控制节点""导航节点");
  • 每个节点有唯一名称,负责完成单一功能(模块化设计),节点间通过 "话题 / 服务 / 动作" 通信,互不干扰。
2. 节点核心特性
特性 说明 应用场景
话题(Topic) 异步通信(发布 / 订阅),一对多 / 多对多 传感器数据发布、状态广播
服务(Service) 同步通信(请求 / 响应),一对一 临时指令(如 "重置计数器""获取参数")
参数(Parameter) 节点的配置项,可动态修改 调整发布频率、设置阈值、配置路径
日志(Logger) 输出调试 / 运行信息(debug/info/warn/error/fatal) 排查问题、监控节点运行状态
生命周期 节点的启动 / 暂停 / 关闭等状态管理 复杂系统的节点状态控制(新手暂用不到)
3. 核心原则
  • 一个节点只做一件事(如 "只发布 TF 坐标""只处理激光数据");
  • 节点名必须唯一(避免冲突);
  • 通信依赖 "接口"(消息 / 服务类型),不同节点需使用相同接口才能通信。
4. 节点基础操作
复制代码
# 1. 运行节点(核心)
ros2 run <功能包名> <节点执行名>
# 示例:运行demo_python_tf包的static_tf_broadcaster节点
ros2 run demo_python_tf static_tf_broadcaster

# 2. 运行节点并修改参数
ros2 run demo_python_tf static_tf_broadcaster --ros-args -p publish_freq:=2.0

# 3. 查看当前运行的所有节点
ros2 node list

# 4. 查看节点详细信息(话题/服务/参数)
ros2 node info /static_tf_broadcaster

# 5. 终止节点(终端按Ctrl+C,或用kill命令)
kill -9 <节点进程ID>  # 先通过ps aux | grep ros2找到进程ID
5. 节点配套调试命令
复制代码
# 1. 查看话题列表(节点发布/订阅的话题)
ros2 topic list

# 2. 订阅并打印话题数据(调试发布节点)
ros2 topic echo /tf  # 查看TF坐标变换数据

# 3. 查看服务列表(节点提供的服务)
ros2 service list

# 4. 调用服务(调试服务节点)
ros2 service call /reset_count std_srvs/srv/SetBool "{data: true}"

# 5. 查看节点参数
ros2 param list  # 列出所有节点的参数
ros2 param get /static_tf_broadcaster publish_freq  # 获取指定参数值
ros2 param set /static_tf_broadcaster publish_freq 3.0  # 动态修改参数值

# 6. 查看日志(调试节点报错)
ros2 logger get /static_tf_broadcaster  # 查看日志级别
ros2 logger set /static_tf_broadcaster DEBUG  # 调整日志级别(显示更多调试信息)
6、 ROS2 节点极简模板

文件名:simple_node.py(放在功能包的 demo_python_tf/ 目录下)

复制代码
#!/usr/bin/env python3
"""ROS2 Python 极简节点模板:发布话题+日志输出"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class SimpleNode(Node):
    def __init__(self):
        # 1. 初始化节点(指定唯一名称)
        super().__init__("simple_node")
        
        # 2. 创建话题发布者(话题名:chatter,消息类型:String)
        self.publisher_ = self.create_publisher(String, "chatter", 10)
        
        # 3. 创建定时器(1秒执行1次回调)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.count = 0
        self.get_logger().info("极简节点已启动!")

    def timer_callback(self):
        # 发布话题数据
        msg = String()
        msg.data = f"Hello ROS2! Count: {self.count}"
        self.publisher_.publish(msg)
        self.get_logger().info(f"发布:{msg.data}")
        self.count += 1

def main(args=None):
    # 初始化ROS2
    rclpy.init(args=args)
    # 创建节点并运行
    node = SimpleNode()
    rclpy.spin(node)
    # 销毁节点
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main(

entry_points={
    'console_scripts': [
        'simple_node = demo_python_tf.simple_node:main',
    ],
},

3.话题

话题(Topic)是 ROS2 中异步、松耦合的通信机制,核心是 "发布者(Publisher)→ 话题 → 订阅者(Subscriber)" 的单向数据流模型:

  • 📤 发布者(Publisher) :主动发送数据的节点(如激光雷达节点发布 /scan 话题);
  • 📥 订阅者(Subscriber) :被动接收数据的节点(如导航节点订阅 /scan 话题);
  • 📨 话题 :数据传输的 "通道",有唯一名称(如 /chatter/tf/scan);
  • 📦 消息(Message) :话题传输的数据格式(如 std_msgs/msg/Stringsensor_msgs/msg/LaserScan),发布 / 订阅双方必须使用相同消息类型才能通信。
特性 说明 典型场景
异步通信 发布者发数据无需等待订阅者响应,订阅者按需接收 传感器数据(激光、图像、IMU)、状态广播
多对多 一个话题可被多个发布者发布、多个订阅者订阅 多个节点发布不同坐标系的 /tf 话题
松耦合 发布者和订阅者无需知道彼此存在,仅依赖话题名和消息类型 替换激光雷达节点,只要仍发布 /scan 话题,导航节点无需修改
1. 消息类型(Topic 的 "数据格式")
  • 标准消息:ROS2 内置的通用消息,如:
    • std_msgs/msg/String:字符串
    • std_msgs/msg/Int32:32 位整数
    • geometry_msgs/msg/Twist:速度指令(线速度 + 角速度)
    • sensor_msgs/msg/LaserScan:激光雷达数据
    • sensor_msgs/msg/Image:图像数据
  • 自定义消息:自己定义的消息格式(需在功能包中创建 .msg 文件并编译)。
2. 话题 QoS

QoS(Quality of Service)是话题的 "通信规则",用于适配不同场景(如实时性、可靠性):

  • 常用配置
    • RELIABLE:确保数据 100% 送达(默认,适合关键数据如控制指令);
    • BEST_EFFORT:尽力送达,不保证(适合高频数据如激光 / 图像,丢几个包不影响);
    • KEEP_LAST:只保留最新的 N 条数据(默认保留 10 条,避免内存溢出)。
  • 核心原则:发布者和订阅者的 QoS 配置需匹配,否则无法通信。
3. 话题命名规则
  • 必须以 / 开头(如 /chatter)或相对路径(如 chatter,会自动拼接节点命名空间);
  • 名称只能包含小写字母、数字、下划线、/
  • 避免重名(不同功能包的话题建议加前缀,如 /demo/chatter)。
4.话题常用命令(高频)
1. 基础查询命令
复制代码
# 1. 查看当前所有活跃话题(最常用)
ros2 topic list

# 2. 查看话题列表,同时显示消息类型(推荐)
ros2 topic list -t

# 3. 查看话题详细信息(发布者/订阅者、QoS、消息类型)
ros2 topic info /chatter

# 4. 查看消息类型的详细结构(知道话题数据包含哪些字段)
ros2 interface show std_msgs/msg/String  # 查看String消息结构
ros2 interface show geometry_msgs/msg/Twist  # 查看Twist消息结构
2. 数据调试命令
复制代码
# 1. 订阅并实时打印话题数据(调试发布者,核心命令)
ros2 topic echo /chatter  # 打印String类型话题
ros2 topic echo /cmd_vel  # 打印Twist类型话题

# 2. 只打印指定字段(避免数据太多,精准调试)
ros2 topic echo /cmd_vel linear.x  # 只打印线速度x轴的值

# 3. 限制打印频率(高频话题如/scan,避免刷屏)
ros2 topic echo /scan --rate 1  # 每秒只打印1次

# 4. 查看话题发布频率(判断发布者是否正常)
ros2 topic hz /chatter  # 显示每秒发布的消息数(Hz)

# 5. 查看话题数据延迟(调试实时性)
ros2 topic delay /chatter
3. 手动发布话题

无需写代码,直接在终端发布话题数据,快速验证订阅者逻辑:

复制代码
# 通用格式:ros2 topic pub <话题名> <消息类型> <数据>
ros2 topic pub /chatter std_msgs/msg/String "{data: 'Hello ROS2'}"

# 持续发布(按频率循环发布,调试运动控制常用)
ros2 topic pub --rate 1 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.2, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.1}}"

# 一次性发布(默认只发1次)
ros2 topic pub --once /chatter std_msgs/msg/String "{data: 'One shot message'}"
4. 话题重映射

运行节点时,把节点原本的话题名映射成新名称,无需修改代码:

复制代码
# 示例:把节点发布的 /chatter 话题重映射为 /demo/chatter
ros2 run demo_python_tf simple_node --ros-args --remap chatter:=demo/chatter

# 同时重映射多个话题
ros2 run demo_python_tf simple_node --ros-args --remap chatter:=demo/chatter --remap cmd_vel:=demo/cmd_vel
5. 话题录制 / 回放
复制代码
# 1. 录制指定话题到bag文件(保存数据)
ros2 bag record /chatter /cmd_vel  # 录制/chatter和/cmd_vel到bag文件
ros2 bag record -o my_bag /chatter  # 指定输出文件名my_bag

# 2. 录制所有话题
ros2 bag record -a

# 3. 回放bag文件(复现数据)
ros2 bag play my_bag_2026_01_25-10_00_00/  # 替换为实际bag目录名
5.示例
复制代码
# 激活环境
source install/setup.bash
# 运行发布节点(发布/chatter话题)
ros2 run demo_python_tf simple_node

# 查看话题列表(能看到/chatter)
ros2 topic list -t
# 输出:/chatter [std_msgs/msg/String]

# 查看话题详细信息
ros2 topic info /chatter
# 输出:发布者1个,订阅者0个,消息类型std_msgs/msg/String

ros2 topic echo /chatter
# 能看到实时打印:data: "Hello ROS2! Count: 0" 等内容

# 新开终端,手动发布/chatter话题
ros2 topic pub --rate 2 /chatter std_msgs/msg/String "{data: 'Manual publish!'}"
# 此时步骤3的终端会同时收到节点发布的和手动发布的消息
发布者
复制代码
#!/usr/bin/env python3
"""ROS2 Python 极简话题发布模板"""
import rclpy
from rclpy.node import Node
# 导入标准字符串消息类型(可替换为其他类型如Int32/Twist)
from std_msgs.msg import String

class SimplePublisher(Node):
    def __init__(self):
        # 1. 初始化节点(命名唯一)
        super().__init__("simple_publisher_node")
        
        # 2. 创建话题发布者
        # 参数说明:话题名、消息类型、队列大小(QoS保留最新10条)
        self.publisher_ = self.create_publisher(String, "chatter", 10)
        
        # 3. 创建定时器(1秒发布1次,周期可调)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.count = 0  # 计数器,用于区分每次发布的消息
        self.get_logger().info("话题发布节点已启动!")

    def timer_callback(self):
        """定时器回调函数:周期性发布话题数据"""
        # 4. 构造消息数据
        msg = String()
        msg.data = f"Hello ROS2! Count: {self.count}"
        
        # 5. 发布消息
        self.publisher_.publish(msg)
        
        # 6. 日志输出(可选,方便调试)
        self.get_logger().info(f"发布消息:{msg.data}")
        self.count += 1

def main(args=None):
    # 初始化ROS2上下文
    rclpy.init(args=args)
    # 创建发布节点实例
    node = SimplePublisher()
    # 自旋节点(保持节点运行,处理定时器回调)
    rclpy.spin(node)
    # 销毁节点+关闭ROS2
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
消费者
复制代码
#!/usr/bin/env python3
"""ROS2 Python 极简话题订阅模板(消费者)"""
import rclpy
from rclpy.node import Node
# 导入和发布者一致的消息类型(必须匹配!)
from std_msgs.msg import String

class SimpleSubscriber(Node):
    def __init__(self):
        # 1. 初始化节点(命名唯一,不能和发布者节点重名)
        super().__init__("simple_subscriber_node")
        
        # 2. 创建话题订阅者
        # 参数说明:话题名(和发布者一致)、消息类型(和发布者一致)、回调函数、队列大小
        self.subscription = self.create_subscription(
            String,
            "chatter",  # 必须和发布者的话题名完全一致
            self.listener_callback,  # 收到消息后执行的回调函数
            10  # QoS队列大小,和发布者匹配即可
        )
        # 防止变量被垃圾回收(可选,但建议加)
        self.subscription  
        self.get_logger().info("话题订阅节点已启动!等待接收消息...")

    def listener_callback(self, msg):
        """订阅回调函数:收到消息时自动执行"""
        # 3. 处理接收到的消息(核心逻辑)
        self.get_logger().info(f"收到消息:{msg.data}")

def main(args=None):
    # 初始化ROS2上下文
    rclpy.init(args=args)
    # 创建订阅节点实例
    node = SimpleSubscriber()
    # 自旋节点(保持节点运行,等待接收消息)
    rclpy.spin(node)
    # 销毁节点+关闭ROS2
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
6.总结
核心知识点
  1. 话题是异步松耦合 通信机制,核心是 "发布者 - 话题 - 订阅者" 模型,依赖统一的消息类型
  2. QoS 是话题的通信规则,发布 / 订阅双方需匹配才能通信;
  3. 话题命名需规范,避免重名,高频数据建议用 BEST_EFFORT QoS。
高频命令

查:ros2 topic list -t(看话题 + 类型)、ros2 topic info(看详情);

调:ros2 topic echo(看数据)、ros2 topic hz(看频率);

测:ros2 topic pub(手动发数据)、--remap(重映射话题名)。

4.服务

服务核心认知

服务(Service)是 ROS2 中同步、请求 - 响应式的通信机制,核心是 "客户端(Client)→ 服务端(Server)" 的双向交互模型,和话题的异步单向通信形成互补:

  • 📞 服务端(Server):提供服务的节点(如 "提供重置计数器服务" 的节点),被动等待客户端请求,处理后返回响应;
  • 📱 客户端(Client):发起请求的节点(如 "请求重置计数器" 的节点),主动发送请求,阻塞等待响应;
  • 📋 服务 :交互的 "接口",有唯一名称(如 /reset_count);
  • 📄 服务类型(srv):定义请求(Request)和响应(Response)的数据格式(如 "请求:是否重置;响应:是否成功 + 提示信息")。
服务 vs 话题
特性 服务(Service) 话题(Topic)
通信模式 同步、请求 - 响应(双向) 异步、发布 - 订阅(单向)
交互时机 按需触发(如 "重置指令") 周期性推送(如 "传感器数据")
阻塞性 客户端会阻塞等待响应 发布 / 订阅均不阻塞
适用场景 指令下发、参数查询、任务触发 数据广播、状态监控
示例 重置计数器、获取节点状态 发布激光数据、速度指令
服务知识点
服务类型(srv)的结构

服务类型文件(.srv)分为 "请求" 和 "响应" 两部分,用 --- 分隔:

  • 标准服务类型

    (ROS2 内置):

    • std_srvs/srv/SetBool:请求(bool data)→ 响应(bool success + string message);
    • std_srvs/srv/Empty:无请求 → 无响应(仅触发动作);
    • rcl_interfaces/srv/SetParameters:设置节点参数的服务类型;
  • 自定义服务类型 :自己创建 .srv 文件,定义请求和响应格式(需在功能包中编译)。

标准服务类型示例

std_srvs/srv/SetBool

复制代码
# 请求部分(客户端发给服务端)
bool data  # 比如:true=重置计数器,false=不重置
---
# 响应部分(服务端返回给客户端)
bool success  # 处理是否成功
string message  # 提示信息(如"重置成功")
服务的核心特性
  • 一对一交互:一个服务请求只能对应一个服务端处理(服务名唯一,避免多个服务端重名);
  • 阻塞性:客户端发送请求后,会阻塞等待服务端响应(可设置超时时间,避免卡死);
  • 无缓存:服务请求 / 响应仅一次有效,不会缓存历史请求;
  • 命名规则 :和话题一致,以 / 开头(如 /reset_count),仅包含小写字母、数字、下划线、/
服务的 QoS

服务的 QoS 比话题简单,核心是 "可靠传输(RELIABLE)",确保请求和响应 100% 送达(ROS2 默认配置,新手无需修改)。

服务常用命令
1. 基础查询命令
复制代码
# 1. 查看当前所有活跃服务(最常用)
ros2 service list

# 2. 查看服务列表,同时显示服务类型(推荐)
ros2 service list -t

# 3. 查看服务详细信息(服务端节点、服务类型)
ros2 service info /reset_count

# 4. 查看服务类型的结构(请求+响应字段)
ros2 interface show std_srvs/srv/SetBool  # 查看SetBool的请求/响应格式
ros2 interface show std_srvs/srv/Empty    # 查看Empty的结构
2. 手动调用服务

无需写代码,直接在终端调用服务,快速验证服务端逻辑:

复制代码
# 通用格式:ros2 service call <服务名> <服务类型> <请求数据>
# 示例1:调用SetBool类型服务(/reset_count)
ros2 service call /reset_count std_srvs/srv/SetBool "{data: true}"

# 示例2:调用Empty类型服务(无请求数据)
ros2 service call /empty_service std_srvs/srv/Empty "{}"

# 示例3:带超时调用(避免等待过久,单位:秒)
ros2 service call /reset_count std_srvs/srv/SetBool "{data: true}" --timeout 5
3. 服务重映射

运行节点时,把节点原本的服务名映射成新名称,无需修改代码:

复制代码
# 示例:把服务端的 /reset_count 重映射为 /demo/reset_count
ros2 run 你的包名 service_server --ros-args --remap reset_count:=demo/reset_count

# 客户端调用时需用新服务名
ros2 service call /demo/reset_count std_srvs/srv/SetBool "{data: true}"
4. 服务端 / 客户端调试辅助命令
复制代码
# 1. 查看节点提供的服务(确认服务端是否正确注册服务)
ros2 node info /service_server_node

# 2. 检查服务是否可用(返回true/false)
ros2 service find std_srvs/srv/SetBool  # 查找所有SetBool类型的服务

# 3. 查看服务通信的日志(调试服务调用失败)
ros2 logger set /service_client_node DEBUG  # 调高客户端日志级别
示例
步骤 1:运行服务端节点
复制代码
# 激活环境
source install/setup.bash
# 运行提供/reset_count服务的节点
ros2 run demo_py_node service_server
# 终端输出:服务端已启动,等待请求...
步骤 2:查看服务信息
复制代码
# 查看服务列表(能看到/reset_count)
ros2 service list -t
# 输出:/reset_count [std_srvs/srv/SetBool]

# 查看服务类型结构
ros2 interface show std_srvs/srv/SetBool
# 输出:
# bool data
# ---
# bool success
# string message
步骤 3:手动调用服务
复制代码
# 调用/reset_count服务,请求重置计数器
ros2 service call /reset_count std_srvs/srv/SetBool "{data: true}"
# 终端返回响应(示例):
# requester: making request: std_srvs.srv.SetBool_Request(data=True)
# response:
# std_srvs.srv.SetBool_Response(success=True, message='计数器已重置为0')
服务端代码
复制代码
#!/usr/bin/env python3
"""ROS2 Python 服务端极简模板(提供重置计数器服务)"""
import rclpy
from rclpy.node import Node
# 导入标准服务类型(SetBool:请求bool,响应success+message)
from std_srvs.srv import SetBool

class SimpleServiceServer(Node):
    def __init__(self):
        # 1. 初始化节点(命名唯一)
        super().__init__("simple_service_server_node")
        
        # 2. 创建服务端
        # 参数说明:服务名、服务类型、回调函数(处理请求)
        self.service = self.create_service(
            SetBool,          # 服务类型(SetBool)
            "reset_count",    # 服务名(客户端需用此名称调用)
            self.reset_count_callback  # 处理请求的回调函数
        )
        
        # 初始化计数器(服务端的核心数据)
        self.count = 0
        self.get_logger().info("服务端已启动!等待重置计数器请求...")

    def reset_count_callback(self, request, response):
        """服务回调函数:处理客户端请求,返回响应"""
        # 3. 处理请求(request.data 是客户端传入的bool值)
        if request.data:
            self.count = 0  # 重置计数器
            response.success = True
            response.message = f"计数器已重置为0"
            self.get_logger().info(f"收到重置请求,计数器已重置:{self.count}")
        else:
            response.success = False
            response.message = f"拒绝重置,当前计数器值:{self.count}"
            self.get_logger().info(f"收到拒绝重置请求,当前值:{self.count}")
        
        # 4. 返回响应(必须返回response对象)
        return response

def main(args=None):
    # 初始化ROS2
    rclpy.init(args=args)
    # 创建服务端节点
    server_node = SimpleServiceServer()
    # 自旋节点(保持运行,等待请求)
    rclpy.spin(server_node)
    # 销毁节点+关闭ROS2
    server_node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
客户端代码
复制代码
#!/usr/bin/env python3
"""ROS2 Python 客户端极简模板(调用重置计数器服务)"""
import rclpy
from rclpy.node import Node
# 导入和服务端一致的服务类型
from std_srvs.srv import SetBool

class SimpleServiceClient(Node):
    def __init__(self):
        # 1. 初始化节点
        super().__init__("simple_service_client_node")
        
        # 2. 创建客户端
        # 参数说明:服务名(和服务端一致)、服务类型
        self.client = self.create_client(SetBool, "reset_count")
        
        # 3. 等待服务端上线(避免调用时服务未就绪)
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().warn("服务端未上线,等待中...")
        
        self.get_logger().info("服务端已上线,可发送请求!")

    def send_request(self, reset_flag):
        """发送服务请求(封装成函数,方便调用)"""
        # 4. 构造请求数据
        request = SetBool.Request()
        request.data = reset_flag  # True=重置,False=不重置
        
        # 5. 发送请求(异步调用,非阻塞;也可用call()同步阻塞)
        self.future = self.client.call_async(request)
        # 等待请求处理完成(阻塞直到收到响应)
        rclpy.spin_until_future_complete(self, self.future)
        
        # 6. 处理响应
        if self.future.result() is not None:
            response = self.future.result()
            self.get_logger().info(f"服务调用结果:success={response.success},message={response.message}")
        else:
            self.get_logger().error("服务调用失败!")

def main(args=None):
    rclpy.init(args=args)
    # 创建客户端节点
    client_node = SimpleServiceClient()
    # 发送重置请求(True=重置,False=不重置)
    client_node.send_request(reset_flag=True)
    # 销毁节点+关闭ROS2
    client_node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
总结
核心知识点
  1. 服务是同步请求 - 响应通信,适用于按需触发的指令交互,和话题的异步广播形成互补;
  2. 服务类型(srv)定义 "请求" 和 "响应" 格式,用 --- 分隔,客户端和服务端必须使用相同类型;
  3. 服务名唯一,一个服务只能有一个服务端,客户端调用时会阻塞等待响应。
高频命令

查:ros2 service list -t(看服务 + 类型)、ros2 interface show(看请求 / 响应结构);

调:ros2 service call(手动调用服务,验证服务端逻辑);

排错:ros2 service info(看服务端信息)、--timeout(避免客户端卡死)。

5.动作

动作(Action)是 ROS2 为长时间任务设计的通信机制,核心是 "动作客户端(Action Client)→ 动作服务端(Action Server)" 的双向交互模型,兼具话题的异步性和服务的请求 - 响应特性,还增加了 "反馈" 和 "取消" 能力:

  • 🎯 动作服务端:执行长时间任务的节点(如 "导航到目标点""机械臂运动"),能实时反馈任务进度,支持接收取消指令;
  • 📱 动作客户端:发起任务请求的节点,可发送目标、接收实时反馈、获取最终结果,也能中途取消任务;
  • 📋 动作 :交互的 "接口",有唯一名称(如 /navigate_to_pose);
  • 📄 动作类型(action):定义 "目标(Goal)、反馈(Feedback)、结果(Result)" 三部分数据格式,是动作的核心约定。
动作 vs 话题 vs 服务
特性 动作(Action) 话题(Topic) 服务(Service)
通信模式 异步、目标 - 反馈 - 结果(双向) 异步、发布 - 订阅(单向) 同步、请求 - 响应(双向)
任务时长 长时间任务(如导航、运动) 无时长限制(高频数据) 短时间任务(单次指令)
核心能力 实时反馈、任务取消、结果返回 数据广播 单次响应
阻塞性 客户端非阻塞(反馈异步接收) 无阻塞 客户端阻塞
适用场景 导航、机械臂运动、路径规划 传感器数据、状态广播 重置计数器、参数查询
动作关键知识点
1. 动作类型(action)的结构

动作类型文件(.action)分为三部分,用 --- 分隔,是动作最核心的约定:

复制代码
# 1. 目标(Goal):客户端发给服务端的任务指令
int32 target_count  # 示例:计数到指定值
---
# 2. 反馈(Feedback):服务端实时返回的进度
int32 current_count  # 示例:当前计数
---
# 3. 结果(Result):任务完成/取消后返回的最终结果
bool success         # 示例:是否成功
string message       # 示例:提示信息
int32 total_count    # 示例:最终计数
  • 标准动作类型

    (ROS2 内置):

    • action_tutorials_interfaces/action/Fibonacci:斐波那契数列计算(官方示例);
    • nav2_msgs/action/NavigateToPose:导航到指定位姿(导航 2 框架核心动作);
  • 自定义动作类型 :自己创建 .action 文件,定义 Goal/Feedback/Result,需在功能包中编译。

2. 动作的核心特性
  • 异步非阻塞:客户端发送目标后无需等待,可继续执行其他逻辑,反馈和结果异步接收;
  • 实时反馈:服务端可周期性返回任务进度(如导航的 "剩余距离""当前位姿");
  • 任务取消:客户端可中途取消任务,服务端收到取消指令后停止任务并返回结果;
  • 目标优先级:支持多个目标排队 / 覆盖(如导航时重新发送目标点,取消原任务);
  • 命名规则 :和话题 / 服务一致,以 / 开头(如 /navigate),仅包含小写字母、数字、下划线、/
3. 动作的 QoS

动作底层基于话题实现,QoS 默认配置为 "可靠传输(RELIABLE)+ 保留最新数据(KEEP_LAST)",确保目标、反馈、结果不丢失,新手无需手动修改。

动作常用命令
1. 基础查询命令
复制代码
# 1. 查看当前所有活跃动作(最常用)
ros2 action list

# 2. 查看动作列表,同时显示动作类型(推荐)
ros2 action list -t

# 3. 查看动作详细信息(服务端节点、动作类型、目标状态)
ros2 action info /fibonacci

# 4. 查看动作类型的结构(Goal/Feedback/Result)
ros2 interface show action_tutorials_interfaces/action/Fibonacci
2. 手动发送动作目标

无需写代码,直接在终端发送动作目标,接收反馈和结果,快速验证动作服务端逻辑:

复制代码
# 通用格式:ros2 action send_goal <动作名> <动作类型> <目标数据> [--feedback]
# 示例1:调用斐波那契数列动作,开启实时反馈
ros2 action send_goal /fibonacci action_tutorials_interfaces/action/Fibonacci "{order: 5}" --feedback

# 示例2:发送目标后中途取消(按Ctrl+C即可触发取消指令)
ros2 action send_goal /count_action demo_interfaces/action/Count "{target_count: 10}" --feedback

# 示例3:仅获取最终结果(不显示反馈)
ros2 action send_goal /count_action demo_interfaces/action/Count "{target_count: 5}"
3. 动作目标管理命令
复制代码
# 1. 查看当前所有动作目标的状态(pending/active/canceled/succeeded)
ros2 action list -s

# 2. 取消指定动作的当前目标(需知道目标ID,新手暂用不到)
ros2 action cancel /fibonacci <goal_id>

# 3. 查找指定类型的所有动作
ros2 action find action_tutorials_interfaces/action/Fibonacci
示例
步骤 1:运行官方动作示例

ROS2 内置了斐波那契数列动作示例,无需自己写代码,直接运行:

复制代码
# 激活环境
source /opt/ros/humble/setup.bash

# 终端1:运行动作服务端
ros2 run action_tutorials_cpp fibonacci_action_server

# 终端2:运行动作客户端(或用命令手动调用)
ros2 run action_tutorials_cpp fibonacci_action_client
# 或手动发送目标(推荐,能看到反馈)
ros2 action send_goal /fibonacci action_tutorials_interfaces/action/Fibonacci "{order: 8}" --feedback
步骤 2:查看执行效果

手动调用后终端会输出:

复制代码
# 目标发送成功
Waiting for an action server to become available...
Sending goal:
   order: 8

# 实时反馈(--feedback 参数生效)
Feedback:
   sequence: [0, 1]
Feedback:
   sequence: [0, 1, 1]
...

# 最终结果
Result:
   sequence: [0, 1, 1, 2, 3, 5, 8, 13, 21]

Goal finished with status: SUCCEEDED
代码
第一步:先创建自定义动作类型

动作依赖 .action 文件定义数据格式,需先在功能包中创建:

1. 创建动作文件目录与文件

在你的 Python 功能包(如 demo_py_node)下新建目录和文件:

复制代码
# 进入功能包目录
cd ~/ros2_ws/src/demo_py_node
# 创建action目录
mkdir action
# 创建计数动作文件
touch action/Count.action
2. 编写 Count.action 文件内容

定义「目标(计数到多少)、反馈(当前数到多少)、结果(是否成功)」:

复制代码
# Goal:客户端发送的目标(计数到target_count)
int32 target_count
---
# Feedback:服务端实时反馈的进度
int32 current_count
---
# Result:任务完成/取消后的最终结果
bool success
string message
int32 total_count
3. 修改 package.xml(添加动作编译依赖)

确保 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>
<!-- 动作运行依赖 -->
<depend>rclpy</depend>
<depend>action_msgs</depend>
4. 修改 setup.py(配置动作文件部署)

setup.py 中添加动作文件的部署逻辑(自动查找 .action 文件):

复制代码
from glob import glob
import os

# 自动查找action文件
action_files = glob(os.path.join('action', '*.action'))

setup(
    # 其他配置不变,在data_files中添加:
    data_files=[
        # 原有配置...
        ('share/' + PACKAGE_NAME + '/action', action_files),
    ],
    # 安装依赖新增action相关
    install_requires=[
        'setuptools>=50.3.0',
        'rclpy>=3.0.0',
        'rosidl_runtime_py',
        'action_msgs',
    ],
)
第二步:动作服务端代码(count_action_server.py)

负责执行「计数任务」,实时反馈进度,支持取消:

复制代码
#!/usr/bin/env python3
"""ROS2 Python 动作服务端示例:计数到指定值"""
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
# 导入自定义动作类型(生成的模块名=包名.action.动作文件名)
from demo_py_node.action import Count

class CountActionServer(Node):
    def __init__(self):
        super().__init__("count_action_server_node")
        # 1. 创建动作服务端
        self._action_server = ActionServer(
            self,
            Count,          # 动作类型
            "count_action", # 动作名(客户端需用此名称调用)
            self.execute_callback  # 执行任务的回调函数
        )
        self.get_logger().info("计数动作服务端已启动!等待计数请求...")

    def execute_callback(self, goal_handle):
        """执行动作任务的核心回调:计数到目标值,实时反馈进度"""
        self.get_logger().info(f"开始计数任务,目标值:{goal_handle.request.target_count}")
        
        # 初始化进度
        feedback_msg = Count.Feedback()
        current_count = 0

        # 循环计数(每秒数1个)
        while current_count < goal_handle.request.target_count:
            # 检查是否收到取消请求
            if goal_handle.is_cancel_requested:
                goal_handle.canceled()
                self.get_logger().info("计数任务被取消!")
                # 返回取消结果
                result = Count.Result()
                result.success = False
                result.message = "任务被取消"
                result.total_count = current_count
                return result

            # 实时更新反馈
            current_count += 1
            feedback_msg.current_count = current_count
            self.get_logger().info(f"当前计数:{current_count}")
            goal_handle.publish_feedback(feedback_msg)

            # 等待1秒(模拟耗时任务)
            rclpy.spin_once(self, timeout_sec=1.0)

        # 任务完成,标记为成功
        goal_handle.succeed()
        self.get_logger().info("计数任务完成!")
        
        # 构造最终结果
        result = Count.Result()
        result.success = True
        result.message = f"成功计数到 {current_count}"
        result.total_count = current_count

        return result

def main(args=None):
    rclpy.init(args=args)
    server_node = CountActionServer()
    rclpy.spin(server_node)
    server_node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
第三步:动作客户端代码(count_action_client.py)

负责发起计数请求,接收实时反馈,支持取消任务:

复制代码
#!/usr/bin/env python3
"""ROS2 Python 动作客户端示例:发起计数请求"""
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
# 导入自定义动作类型
from demo_py_node.action import Count

class CountActionClient(Node):
    def __init__(self):
        super().__init__("count_action_client_node")
        # 1. 创建动作客户端
        self._action_client = ActionClient(self, Count, "count_action")
        self.get_logger().info("计数动作客户端已启动!")

    def send_goal(self, target_count):
        """发送计数目标,并处理反馈和结果"""
        # 构造目标请求
        goal_msg = Count.Goal()
        goal_msg.target_count = target_count

        # 2. 等待服务端上线
        self.get_logger().info("等待动作服务端上线...")
        self._action_client.wait_for_server()

        # 3. 发送目标(异步),绑定反馈和结果回调
        self._send_goal_future = self._action_client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback  # 实时反馈回调
        )

        # 4. 等待目标发送结果,并绑定任务完成回调
        self._send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        """目标发送结果回调:确认服务端是否接受目标"""
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().error("计数请求被服务端拒绝!")
            return

        self.get_logger().info("计数请求已被接受,等待任务完成...")

        # 5. 等待任务完成,获取最终结果
        self._get_result_future = goal_handle.get_result_async()
        self._get_result_future.add_done_callback(self.get_result_callback)

        # 可选:模拟5秒后取消任务(注释掉则完成全部计数)
        # self.create_timer(5.0, lambda: goal_handle.cancel_goal_async())

    def feedback_callback(self, feedback_msg):
        """实时反馈回调:接收服务端的进度"""
        feedback = feedback_msg.feedback
        self.get_logger().info(f"收到进度反馈:当前计数={feedback.current_count}")

    def get_result_callback(self, future):
        """最终结果回调:处理任务完成/取消的结果"""
        result = future.result().result
        status = future.result().status
        if status == 3:  # SUCCEEDED
            self.get_logger().info(f"任务成功!结果:{result.message},总计数:{result.total_count}")
        elif status == 2:  # CANCELED
            self.get_logger().info(f"任务被取消!已计数:{result.total_count}")
        else:
            self.get_logger().error(f"任务失败!状态码:{status}")

        # 任务完成后关闭节点
        rclpy.shutdown()

def main(args=None):
    rclpy.init(args=args)
    client_node = CountActionClient()
    # 发送计数请求(目标数到10)
    client_node.send_goal(target_count=10)
    # 自旋节点,等待回调执行
    rclpy.spin(client_node)
    client_node.destroy_node()

if __name__ == "__main__":
    main()
第四步:配置节点入口与运行
1. 修改 setup.py 的 entry_points

添加动作服务端 / 客户端的运行入口:

复制代码
entry_points={
    'console_scripts': [
        # 动作服务端
        'count_action_server = demo_py_node.count_action_server:main',
        # 动作客户端
        'count_action_client = demo_py_node.count_action_client:main',
    ],
},
2. 编译与运行
复制代码
# 1. 编译功能包(首次编译自定义动作需完整编译)
cd ~/ros2_ws
colcon build --packages-select demo_py_node --symlink-install
source install/setup.bash

# 2. 终端1:运行动作服务端
ros2 run demo_py_node count_action_server

# 3. 终端2:运行动作客户端
ros2 run demo_py_node count_action_client

# 可选:手动发送动作目标(验证服务端)
ros2 action send_goal /count_action demo_py_node/action/Count "{target_count: 5}" --feedback
总结
核心知识点
  1. 动作是为长时间任务 设计的通信机制,核心能力是实时反馈、任务取消、异步结果返回
  2. 动作类型(.action)包含 Goal(目标)、Feedback(反馈)、Result(结果)三部分,客户端和服务端必须使用相同类型;
  3. 动作兼具话题的异步性和服务的交互性,是导航、机械臂运动等场景的首选通信方式。
高频命令

查:ros2 action list -t(看动作 + 类型)、ros2 interface show(看 Goal/Feedback/Result 结构);

调:ros2 action send_goal <动作名> <类型> <目标> --feedback(手动发送目标,查看反馈);

排错:ros2 action info(看服务端信息)、ros2 action list -s(看目标状态)。

6.launch

Launch 核心认知

Launch(启动文件)是 ROS2 为复杂系统启动设计的工具,核心作用是:

  • 🚀 批量启动多个节点(如同时启动 "导航节点 + 激光节点 + 控制节点");
  • ⚙️ 统一配置节点参数、话题重映射、命名空间;
  • 🧩 管理节点依赖(如 "先启动地图节点,再启动导航节点");
  • 📌 动态控制节点生命周期(如 "某个节点崩溃后自动重启")。
Launch 优势
手动启动节点 Launch 启动节点
需打开多个终端,逐个运行 ros2 run 一个终端运行 ros2 launch,批量启动所有节点
每次启动需重复输入参数 / 重映射指令 参数 / 重映射配置写在 Launch 文件中,一次编写永久复用
无依赖管理,节点启动顺序混乱 可指定节点启动顺序,避免 "依赖节点未启动导致报错"
Launch 关键知识点
1. Launch 文件的类型
类型 后缀 特点 适用场景
Python 版 .launch.py 灵活、支持逻辑控制(if/for)、ROS2 官方推荐 绝大多数场景(新手首选)
XML 版 .launch.xml 结构化强、语法简洁 简单场景(无复杂逻辑)
YAML 版 .launch.yaml 配置化、易读 纯节点启动(无逻辑)
2. Launch 的核心组件

Launch 文件的核心是通过 "动作(Action)" 和 "描述器(Descriptor)" 定义启动逻辑,常用组件:

  • Node:启动单个 ROS2 节点(核心组件);
  • Parameter:为节点设置参数;
  • Remap:为节点做话题 / 服务重映射;
  • Namespace:为节点设置命名空间(避免节点名 / 话题名冲突);
  • IncludeLaunchDescription:嵌套引入其他 Launch 文件(模块化);
  • SetEnvironmentVariable:设置环境变量(如日志级别);
  • LaunchConfiguration:接收命令行传入的参数(动态配置)。
3. Launch 文件的部署规则
  • Launch 文件需放在功能包的 launch/ 目录下;
  • 编译时需在 setup.py(Python 包)或 CMakeLists.txt(C++ 包)中配置,将 launch/ 目录安装到 install/包名/share/包名/launch/ 路径;
  • 运行时通过 ros2 launch 包名 文件名.launch.py 调用,无需指定路径。
4. 节点启动策略
  • respawn=True:节点崩溃后自动重启(如关键传感器节点);
  • respawn_delay=5:重启延迟 5 秒(避免频繁重启);
  • output='screen':节点日志输出到终端(调试必备);
  • arguments:给节点传递命令行参数(如 --ros-args -p freq:=2.0)。
Python 版 Launch 编写模板

以下是覆盖 "多节点启动、参数配置、话题重映射、命名空间、命令行传参" 的通用模板,基于之前的 "话题发布 / 订阅、服务、动作" 节点编写:

1. 基础模板

demo_launch.py

放在功能包的 launch/ 目录下:

复制代码
#!/usr/bin/env python3
"""ROS2 Python Launch 通用模板:批量启动多节点"""
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution

def generate_launch_description():
    # 1. 声明命令行参数(可选,动态配置)
    count_arg = DeclareLaunchArgument(
        'target_count',  # 参数名
        default_value=TextSubstitution(text='10'),  # 默认值
        description='计数动作的目标值'  # 描述
    )

    # 2. 定义节点1:话题发布节点
    publisher_node = Node(
        package='demo_py_node',  # 功能包名
        executable='topic_publisher',  # 节点执行名(setup.py中定义的)
        name='publisher_node',  # 节点名(覆盖代码中的节点名)
        namespace='demo',  # 命名空间(话题会变成 /demo/chatter)
        output='screen',  # 日志输出到终端
        respawn=True,  # 崩溃自动重启
        respawn_delay=3,  # 重启延迟3秒
        # 话题重映射(将节点的 /chatter 重映射为 /demo/chat)
        remappings=[
            ('chatter', 'chat')
        ],
        # 节点参数(等价于 --ros-args -p publish_freq:=2.0)
        parameters=[{
            'publish_freq': 2.0,
            'max_count': 100
        }]
    )

    # 3. 定义节点2:话题订阅节点
    subscriber_node = Node(
        package='demo_py_node',
        executable='topic_subscriber',
        name='subscriber_node',
        namespace='demo',
        output='screen',
        remappings=[
            ('chatter', 'chat')  # 和发布者重映射一致
        ]
    )

    # 4. 定义节点3:动作服务端节点(使用命令行参数)
    action_server_node = Node(
        package='demo_py_node',
        executable='count_action_server',
        name='count_action_server_node',
        output='screen',
        # 传递命令行参数到节点(可选)
        arguments=['--ros-args', '-p', f'target_count:={LaunchConfiguration("target_count")}']
    )

    # 5. 组装所有启动动作
    return LaunchDescription([
        count_arg,  # 先声明命令行参数
        publisher_node,
        subscriber_node,
        action_server_node
        # 可添加更多节点...
    ])
2. 模板关键组件说明
组件 作用 示例
DeclareLaunchArgument 声明命令行参数,支持动态配置 target_count 参数,默认值 10
Node(package/executable) 指定要启动的节点(功能包 + 执行名) package='demo_py_node', executable='topic_publisher'
name 覆盖节点代码中的默认节点名,避免冲突 name='publisher_node'
namespace 给节点加命名空间,话题变为 /命名空间/话题名 namespace='demo' → 话题 /demo/chatter
remappings 话题 / 服务重映射,列表形式,元素为元组 ('chatter', 'chat') → 把 /chatter 改成 /chat
parameters 给节点设置参数,字典形式 {'publish_freq': 2.0} → 等价于 --ros-args -p publish_freq:=2.0
output='screen' 节点日志输出到终端(调试必备) -
respawn=True 节点崩溃自动重启 -
Launch 文件的部署与运行
1. 部署 Launch 文件

Python 功能包(setup.py 配置)

setup.pydata_files 中添加 launch 目录部署:

复制代码
from glob import glob
import os

setup(
    # 其他配置不变
    data_files=[
        # 原有配置(如package.xml、action文件)...
        # 部署launch目录
        ('share/' + PACKAGE_NAME + '/launch', glob(os.path.join('launch', '*.launch.py'))),
    ],
)

C++ 功能包(CMakeLists.txt 配置)

复制代码
# 部署launch目录
install(DIRECTORY
  launch
  DESTINATION share/${PROJECT_NAME}/
)
2. 常用 Launch 运行命令
复制代码
# 1. 基础运行(使用默认参数)
ros2 launch demo_py_node demo_launch.py

# 2. 传递命令行参数(覆盖默认值)
ros2 launch demo_py_node demo_launch.py target_count:=15

# 3. 查看Launch文件的帮助信息(查看可传的参数)
ros2 launch demo_py_node demo_launch.py --show-args

# 4. 启动时指定日志级别
ros2 launch demo_py_node demo_launch.py --ros-args --log-level DEBUG

# 5. 仅打印Launch文件的执行计划(不实际启动节点,调试用)
ros2 launch demo_py_node demo_launch.py --dry-run
3. 运行效果

执行 ros2 launch demo_py_node demo_launch.py 后:

  • 终端会批量启动 publisher_nodesubscriber_nodecount_action_server_node
  • 所有节点的日志都会输出到当前终端(因 output='screen');
  • 话题会被重映射为 /demo/chat,避免和其他节点冲突;
  • publisher_node 崩溃,会在 3 秒后自动重启。
Launch 进阶技巧
1. 嵌套引入其他 Launch 文件

如果有多个 Launch 文件(如 sensor_launch.pynav_launch.py),可在主 Launch 文件中嵌套引入:

复制代码
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os

# 引入其他Launch文件
sensor_launch = IncludeLaunchDescription(
    PythonLaunchDescriptionSource(
        os.path.join(
            get_package_share_directory('demo_py_node'),  # 功能包路径
            'launch/sensor_launch.py'  # 嵌套的Launch文件
        )
    ),
    # 给嵌套的Launch文件传参数
    launch_arguments={'sensor_type': 'lidar', 'freq': '10.0'}.items()
)

# 在LaunchDescription中添加
return LaunchDescription([
    sensor_launch,
    # 其他节点...
])
2. 条件启动节点

根据命令行参数决定是否启动某个节点(如仅在调试模式启动 RViz):

复制代码
from launch.conditions import IfCondition

# 声明调试模式参数
debug_arg = DeclareLaunchArgument(
    'debug',
    default_value='false',
    description='是否启动调试模式(启动RViz)'
)

# 条件启动RViz节点
rviz_node = Node(
    package='rviz2',
    executable='rviz2',
    name='rviz2',
    output='screen',
    condition=IfCondition(LaunchConfiguration('debug'))  # 仅debug=true时启动
)

# 组装
return LaunchDescription([
    debug_arg,
    rviz_node,
    # 其他节点...
])

# 运行时启动调试模式:
# ros2 launch demo_py_node demo_launch.py debug:=true
总结
核心知识点
  1. Launch 是 ROS2 批量启动节点的工具,推荐使用 Python 版(.launch.py),支持逻辑控制和动态配置;
  2. 核心组件是 Node(启动节点)、Remap(重映射)、Parameter(参数)、LaunchConfiguration(命令行参数);
  3. Launch 文件需部署到 share/包名/launch/ 目录,运行命令为 ros2 launch 包名 文件名.launch.py
高频操作

调试必备:output='screen'(日志输出到终端)、--dry-run(预览启动计划);

动态配置:用 DeclareLaunchArgument 声明命令行参数,LaunchConfiguration 引用;

模块化:用 IncludeLaunchDescription 嵌套引入其他 Launch 文件,拆分复杂启动逻辑。

7.tf

TF2 核心认知

TF2(Transform Library 2)是 ROS2 用于跟踪、广播、查询多坐标系之间变换关系的框架,核心解决 "不同传感器 / 部件的坐标统一" 问题:

  • 🧭 坐标系(Frame) :每个部件 / 传感器对应一个唯一坐标系(如 base_link(机器人基坐标)、laser(激光雷达坐标)、map(地图坐标));
  • 📡 TF 广播器(TF Broadcaster):发布两个坐标系之间的变换关系(如 "激光雷达相对于基坐标的位置");
  • 🔍 TF 监听器(TF Listener):查询任意两个坐标系之间的变换关系(如 "从激光雷达坐标转换到地图坐标");
  • 时间戳:TF2 会缓存变换关系(默认 10 秒),支持查询 "历史时刻" 的坐标变换(如 "1 秒前激光雷达相对于地图的位置")。
TF2 解决的核心问题

机器人上的传感器(激光、相机、IMU)都有自己的局部坐标系,TF2 能将这些局部坐标统一到全局坐标系(如地图),比如:

  • 激光雷达检测到障碍物在自身坐标系的 (1,0,0) 位置 → 通过 TF2 转换为地图坐标系的 (10,5,0)
  • 导航指令要求机器人移动到地图坐标系的 (20,8,0) → 通过 TF2 转换为机器人基坐标的运动指令。
TF2 关键知识点
1. 坐标系命名规范

为避免混乱,ROS2 对常用坐标系有统一命名,新手建议遵循:

坐标系名 含义 示例
map 全局地图坐标系(固定) 导航的全局参考系
odom 里程计坐标系(随机器人移动) 基于轮式里程计的局部参考系
base_link 机器人基坐标系(核心) 机器人底盘中心
base_footprint 机器人底盘接地坐标系 用于地面高度参考
laser/lidar 激光雷达坐标系 激光雷达安装位置
camera 相机坐标系 相机光心位置
2. TF2 变换的两种类型
变换类型 特点 适用场景
静态变换(Static TF) 变换关系永久不变 传感器相对于机器人基坐标的安装位置(如激光雷达固定在底盘)
动态变换(Dynamic TF) 变换关系实时变化 机器人基坐标相对于地图的位置(导航时实时更新)
3. TF2 的核心通信机制

TF2 底层基于 话题(Topic) 实现:

  • 静态变换:通过 /tf_static 话题发布(QoS 为 "永久保留",只发一次);
  • 动态变换:通过 /tf 话题发布(周期性更新);
  • 所有变换关系可通过 ros2 topic echo /tfros2 topic echo /tf_static 查看。
4. TF2 变换的表示方式

TF2 中的坐标变换包含平移(Translation)旋转(Rotation)

  • 平移:用 x/y/z 表示坐标系原点的偏移(如激光雷达在基坐标的 x=0.2, y=0, z=0.1);
  • 旋转:常用四元数(x/y/z/w)或欧拉角(滚转 / 俯仰 / 偏航)表示,ROS2 优先用四元数(避免万向锁)。
5. TF2 的核心概念:

变换树(TF Tree)

所有坐标系通过 "父子关系" 组成一棵变换树,必须无环、无孤立节点,比如:

复制代码
map → odom → base_link → laser
            ↳ base_link → camera
  • mapodom 的父节点,odombase_link 的父节点;
  • 只能查询 "变换树中存在路径" 的两个坐标系(如 maplaser 可行,mapimu 需先添加 base_link → imu 变换)。
TF2 常用工具 / 命令
1. 基础查询工具
(1)查看所有坐标系(tf2_tools)
复制代码
# 安装tf2工具(若未安装)
sudo apt install ros-humble-tf2-tools

# 查看当前所有坐标系及父子关系(可视化变换树)
ros2 run tf2_tools view_frames
# 生成frames.pdf文件,用PDF阅读器打开即可看到变换树
evince frames.pdf
(2)查询两个坐标系的变换关系(最常用)
复制代码
# 通用格式:ros2 run tf2_ros tf2_echo <父坐标系> <子坐标系>
# 示例1:查询激光雷达(laser)相对于基坐标(base_link)的变换(静态)
ros2 run tf2_ros tf2_echo base_link laser

# 示例2:查询基坐标(base_link)相对于地图(map)的变换(动态)
ros2 run tf2_ros tf2_echo map base_link

# 输出说明:
# - Translation:平移(x/y/z)
# - Rotation:旋转(四元数+欧拉角)
# 动态变换会实时刷新,静态变换只输出一次
2. 话题调试工具
复制代码
# 1. 查看TF话题列表
ros2 topic list | grep tf  # 输出 /tf /tf_static

# 2. 查看TF话题数据(静态变换)
ros2 topic echo /tf_static

# 3. 查看TF话题数据(动态变换)
ros2 topic echo /tf

# 4. 查看TF话题发布频率(动态变换)
ros2 topic hz /tf
3. 静态变换发布工具

新手无需写代码,可直接用命令发布静态 TF(适合调试传感器安装位置):

复制代码
# 通用格式:ros2 run tf2_ros static_transform_publisher x y z yaw pitch roll 父坐标系 子坐标系
# 参数说明:x/y/z(平移),yaw/pitch/roll(旋转,单位:弧度,绕z/y/x轴)
# 示例:发布laser相对于base_link的静态变换(x=0.2, y=0, z=0.1,无旋转)
ros2 run tf2_ros static_transform_publisher 0.2 0 0.1 0 0 0 base_link laser

# 可选:用四元数指定旋转(适合复杂旋转)
ros2 run tf2_ros static_transform_publisher x y z qx qy qz qw 父坐标系 子坐标系
4. RViz2 可视化 TF

RViz2 是 TF2 可视化的核心工具,能直观看到所有坐标系的位置关系:

复制代码
# 启动RViz2
rviz2

# 在RViz2中配置TF可视化:
# 1. 左侧Displays面板,点击Add → 选择TF → OK;
# 2. 勾选TF的Show Names(显示坐标系名)、Show Axes(显示坐标轴);
# 3. 设置Fixed Frame为map(全局参考系),即可看到所有坐标系的位置。
5. 动态变换调试工具
复制代码
# 查看TF缓存状态(调试历史变换查询)
ros2 run tf2_ros tf2_monitor base_link laser

# 检查TF变换树是否有错误(如环、孤立节点)
ros2 run tf2_tools check_tf_tree
TF2 代码开发核心模板
1. 静态 TF 发布
复制代码
#!/usr/bin/env python3
"""ROS2 静态TF发布模板"""
import rclpy
from rclpy.node import Node
from tf2_ros import StaticTransformBroadcaster
from geometry_msgs.msg import TransformStamped
import tf_transformations  # 用于四元数转换

class StaticTFBroadcaster(Node):
    def __init__(self):
        super().__init__("static_tf_broadcaster_node")
        # 创建静态TF广播器
        self.tf_broadcaster = StaticTransformBroadcaster(self)
        
        # 构造变换关系
        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = "base_link"  # 父坐标系
        t.child_frame_id = "laser"       # 子坐标系
        
        # 设置平移(x=0.2, y=0, z=0.1)
        t.transform.translation.x = 0.2
        t.transform.translation.y = 0.0
        t.transform.translation.z = 0.1
        
        # 设置旋转(无旋转,四元数为(0,0,0,1))
        q = tf_transformations.quaternion_from_euler(0, 0, 0)
        t.transform.rotation.x = q[0]
        t.transform.rotation.y = q[1]
        t.transform.rotation.z = q[2]
        t.transform.rotation.w = q[3]
        
        # 发布静态TF(只发一次)
        self.tf_broadcaster.sendTransform(t)
        self.get_logger().info("已发布base_link → laser的静态TF")

def main(args=None):
    rclpy.init(args=args)
    node = StaticTFBroadcaster()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
2. 动态 TF 发布
复制代码
#!/usr/bin/env python3
"""ROS2 动态TF发布模板(模拟base_link→odom的动态变换)"""
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
import tf_transformations

class DynamicTFBroadcaster(Node):
    def __init__(self):
        super().__init__("dynamic_tf_broadcaster_node")
        # 创建动态TF广播器
        self.tf_broadcaster = TransformBroadcaster(self)
        # 定时器(10Hz发布动态TF)
        self.timer = self.create_timer(0.1, self.timer_callback)
        self.x = 0.0  # 模拟x轴平移递增

    def timer_callback(self):
        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = "odom"       # 父坐标系
        t.child_frame_id = "base_link"   # 子坐标系
        
        # 动态更新平移(x轴每秒移动0.1m)
        t.transform.translation.x = self.x
        t.transform.translation.y = 0.0
        t.transform.translation.z = 0.0
        
        # 旋转(无旋转)
        q = tf_transformations.quaternion_from_euler(0, 0, 0)
        t.transform.rotation.x = q[0]
        t.transform.rotation.y = q[1]
        t.transform.rotation.z = q[2]
        t.transform.rotation.w = q[3]
        
        # 发布动态TF
        self.tf_broadcaster.sendTransform(t)
        self.x += 0.01  # 每次回调x增加0.01
        self.get_logger().info(f"发布动态TF:odom → base_link,x={self.x:.2f}")

def main(args=None):
    rclpy.init(args=args)
    node = DynamicTFBroadcaster()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
3. TF 监听
复制代码
#!/usr/bin/env python3
"""ROS2 TF监听模板(查询laser→map的变换)"""
import rclpy
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener
import tf2_ros

class TFListener(Node):
    def __init__(self):
        super().__init__("tf_listener_node")
        # 创建TF缓存和监听器
        self.tf_buffer = Buffer()
        self.tf_listener = TransformListener(self.tf_buffer, self)
        # 定时器(1Hz查询变换)
        self.timer = self.create_timer(1.0, self.timer_callback)

    def timer_callback(self):
        try:
            # 查询laser相对于map的变换(最新时刻)
            # 参数:子坐标系、父坐标系、查询时间(rclpy.time.Time()表示最新)
            trans = self.tf_buffer.lookup_transform(
                "map",
                "laser",
                rclpy.time.Time()
            )
            self.get_logger().info(f"laser → map 平移:x={trans.transform.translation.x:.2f}")
        except tf2_ros.TransformException as e:
            self.get_logger().warn(f"无法查询变换:{e}")

def main(args=None):
    rclpy.init(args=args)
    node = TFListener()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == "__main__":
    main()
常见问题排查

tf2_echo 查询失败 "Could not find a connection between [map] and [laser]"

  • 检查变换树是否完整(如 map → odom → base_link → laser 路径是否存在);
  • 检查 TF 广播器是否运行,坐标系名是否拼写正确(区分大小写);
  • view_frames 查看变换树,确认无孤立节点。

静态 TF 发布后无效果

  • 静态 TF 发布命令的参数顺序是否正确(x y z yaw pitch roll 父 子);
  • 检查 /tf_static 话题是否有数据(ros2 topic echo /tf_static)。

RViz2 中看不到坐标系

  • 确认 Fixed Frame 设置正确(如 map);
  • 勾选 TF 面板的 Show Axes 和 Show Names;
  • 检查 TF 广播器是否正常发布变换。
总结
核心知识点
  1. TF2 是 ROS2 处理坐标系变换的核心框架,分为静态 TF (固定变换)和动态 TF(实时变换);
  2. 所有坐标系组成变换树,必须无环、无孤立节点,才能查询任意两坐标系的变换;
  3. TF2 底层基于 /tf(动态)和 /tf_static(静态)话题发布变换关系。
高频工具 / 命令
  1. 查变换树:ros2 run tf2_tools view_frames + evince frames.pdf
  2. 查变换关系:ros2 run tf2_ros tf2_echo 父坐标系 子坐标系
  3. 发布静态 TF:ros2 run tf2_ros static_transform_publisher x y z yaw pitch roll 父 子
  4. 可视化:RViz2 中添加 TF 显示,设置 Fixed Frame 为全局坐标系。
开发核心
  1. 静态 TF:用 StaticTransformBroadcaster 发布(只发一次);
  2. 动态 TF:用 TransformBroadcaster + 定时器周期性发布;
  3. 查变换:用 Buffer + TransformListenerlookup_transform 方法。

8.rqt

RQT 核心认知

RQT(ROS Qt GUI)是 ROS2 基于 Qt 开发的模块化图形化工具框架,核心特点:

  • 🎨 可视化调试:用图形界面替代终端命令,直观查看话题数据、节点关系、TF 变换等;
  • 🔩 插件化设计 :RQT 本身是框架,功能通过 "插件" 实现(如 rqt_graph 查看节点图、rqt_plot 绘制数据曲线);
  • 🚀 跨平台:支持 Linux/Windows/macOS,和 ROS2 核心功能深度集成;
  • 📌 一键启动 :通过 rqt 命令启动主界面,或直接启动单个插件(如 rqt_graph)。
RQT 优势
终端调试 RQT 可视化调试
需记忆大量命令(如 ros2 topic echo/ros2 param set 点选操作,无需记命令
纯文本输出,数据无直观性(如高频数值) 曲线 / 图表 / 树形结构展示,数据趋势一目了然
单次操作(如改参数需重新输入命令) 实时交互(如拖动滑块改参数,即时生效)
RQT 知识点
1. RQT 的安装

ROS2 桌面版(ros-humble-desktop)已默认安装 RQT,若未安装可手动补全:

复制代码
# 安装RQT核心包
sudo apt install ros-humble-rqt
# 安装常用插件(推荐)
sudo apt install ros-humble-rqt-common-plugins ros-humble-rqt-tf-tree
2. RQT 的启动方式
启动方式 命令 适用场景
启动主界面(所有插件) rqt 需同时使用多个插件(如看节点图 + 改参数)
启动单个插件 rqt_xxx(如 rqt_graph/rqt_plot 仅需单个功能,启动更快
启动指定插件(主界面) rqt --plugin rqt_graph 主界面默认打开指定插件
3. RQT 插件的通用操作
  • 插件切换 :主界面顶部 Plugins 菜单 → 选择对应插件(如 Visualization → Graph);
  • 重置布局Window → Reset Perspective(界面乱了时恢复默认);
  • 保存布局Window → Save Perspective As(自定义布局,下次直接加载);
  • 关闭插件 :插件窗口右上角 ×,或 Plugins → 已勾选插件 取消勾选。
4. RQT 与 ROS2 核心功能的映射

RQT 插件本质是封装了 ROS2 的终端命令,核心映射关系:

ROS2 终端命令 RQT 对应插件 功能
ros2 node list/ros2 topic list rqt_graph 可视化节点 / 话题 / 服务的关系图
ros2 topic echo/ros2 topic hz rqt_plot/rqt_topic 查看 / 绘制话题数据
ros2 param set/ros2 param get rqt_reconfigure(或 rqt_param 实时修改 / 查看节点参数
ros2 service call rqt_service_caller 可视化调用服务
ros2 action send_goal rqt_action 可视化发送动作目标
ros2 run tf2_tools view_frames rqt_tf_tree 可视化 TF 变换树
RQT 常用工具(插件)详解
1. rqt_graph
作用

直观展示当前运行的节点、话题、服务、动作之间的连接关系,是调试 "节点是否通信" 的首选工具。

复制代码
# 直接启动
rqt_graph
# 或从RQT主界面启动:Plugins → Visualization → Graph
关键操作
  • 刷新:点击左上角刷新按钮(或按 F5),更新节点图;

  • 过滤

    • Nodes only:只显示节点(隐藏话题 / 服务);
    • Hide debug:隐藏调试类话题(如 /rosout);
    • 搜索框输入关键词(如 chatter),过滤相关节点 / 话题;
  • 缩放 / 平移:鼠标滚轮缩放,左键拖动平移;

  • 节点颜色:不同颜色代表不同节点,连线代表话题 / 服务通信。

实操示例

运行之前的话题发布 / 订阅节点,启动 rqt_graph

  • 能看到 publisher_nodesubscriber_node 之间有连线(对应 /chatter 话题);
  • 连线标注 std_msgs/msg/String,表示通信的消息类型;
  • 验证节点是否正确建立通信(无连线则说明话题名 / 类型不匹配)。
2. rqt_plot(数值话题曲线绘制)
作用

实时绘制数值型话题 的曲线(如 std_msgs/msg/Int32geometry_msgs/msg/Twist 的 x 轴速度),直观查看数据趋势。

启动与使用
复制代码
# 直接启动
rqt_plot
# 或从RQT主界面启动:Plugins → Visualization → Plot
关键操作
  • 添加话题

    :在顶部输入框填写话题路径(如

    复制代码
    /chatter/data

    复制代码
    /cmd_vel/linear/x

    ),按回车添加;

    • 支持嵌套字段:如 /twist/linear/x(Twist 消息的线速度 x 轴);
  • 调整坐标轴

    • 鼠标滚轮缩放 Y 轴,右键拖动平移;
    • 点击 Auto-scale 自动适配数据范围;
  • 数据保存File → Save Data,将曲线数据保存为 CSV 文件;

  • 清除曲线 :选中曲线 → 右键 → Remove

实操示例

运行发布 Twist 消息的节点,在 rqt_plot 中输入 /cmd_vel/linear/x

  • 能看到曲线实时跟随线速度值变化;
  • 验证速度指令是否稳定(曲线抖动 / 突变则说明指令异常)。
3. rqt_topic(话题综合调试)
作用

一站式查看所有话题的发布频率、消息类型、实时数据 ,替代 ros2 topic list/echo/hz 等命令。

启动与使用
复制代码
# 直接启动
rqt_topic
# 或从RQT主界面启动:Plugins → Topics → Topic Monitor
关键操作
  • 查看话题列表:左侧列表显示所有活跃话题,包含消息类型、发布者 / 订阅者数量;
  • 实时查看数据 :选中话题 → 右侧 Message 标签页,实时显示消息内容;
  • 查看发布频率Hz 标签页,显示话题的发布频率(等价于 ros2 topic hz);
  • 手动发布话题Publish 标签页,填写消息数据 → 点击 Publish,替代 ros2 topic pub 命令。
4. rqt_reconfigure(参数动态调整)
作用

可视化调整节点的动态参数 (需节点使用 rclpy.parameterdynamic_reconfigure),实时生效无需重启节点。

启动与使用
复制代码
# 直接启动
rqt_reconfigure
# 或从RQT主界面启动:Plugins → Configuration → Dynamic Reconfigure
关键操作
  • 选择节点 :左侧列表选择要调整的节点(如 publisher_node);

  • 调整参数

    • 数值型参数:拖动滑块 / 直接输入值,自动生效;
    • 布尔型参数:勾选 / 取消勾选;
    • 枚举型参数:下拉选择;
  • 保存参数File → Save Config,将参数配置保存为文件,下次加载使用。

注意

仅支持标记为 "动态可修改" 的参数,若节点未配置动态参数,此插件中看不到对应节点。

5. rqt_service_caller(服务可视化调用)
作用

可视化调用服务,替代 ros2 service call 命令,无需手动编写请求数据格式。

启动与使用
复制代码
# 直接启动
rqt_service_caller
# 或从RQT主界面启动:Plugins → Services → Service Caller
关键操作
  • 选择服务 :下拉框选择要调用的服务(如 /reset_count);
  • 填写请求数据 :自动生成请求表单(如 SetBool 服务的 data 复选框);
  • 调用服务 :点击 Call,右侧显示响应结果(成功 / 失败 + 消息);
  • 重复调用 :勾选 Repeat,设置调用频率(如 1Hz),自动重复调用。
6. rqt_tf_tree(TF 变换树可视化)
作用

直观展示 TF 变换树的父子关系、坐标系位置 ,替代 ros2 run tf2_tools view_frames 命令,支持实时刷新。

启动与使用
复制代码
# 直接启动
rqt_tf_tree
# 或从RQT主界面启动:Plugins → Visualization → TF Tree
关键操作
  • 刷新变换树 :点击 Refresh,实时更新坐标系关系;
  • 查看变换详情:鼠标悬停在连线上,显示平移 / 旋转参数;
  • 导出图片File → Save as,将变换树保存为 PNG/PDF 文件;
  • 设置参考系 :下拉框选择 Fixed Frame,调整可视化的参考坐标系。
7. rqt_console(日志可视化查看)
作用

集中查看所有节点的日志信息(debug/info/warn/error/fatal),支持过滤、搜索、保存日志。

启动与使用
复制代码
# 直接启动
rqt_console
# 或从RQT主界面启动:Plugins → Logging → Console
关键操作
  • 过滤日志级别 :勾选顶部 Debug/Info/Warn/Error,只显示对应级别的日志;
  • 搜索日志 :输入关键词(如 count),过滤包含该关键词的日志;
  • 保存日志File → Save Log,将日志保存为文件,用于离线分析;
  • 清除日志 :点击 Clear,清空当前日志列表。
RQT 进阶技巧
1. 自定义 RQT 布局

若经常同时使用多个插件(如 rqt_graph + rqt_plot + rqt_console),可自定义布局并保存:

复制代码
# 1. 启动RQT主界面,打开需要的插件,调整窗口位置;
# 2. 保存布局:Window → Save Perspective As → 命名(如 `debug_layout`);
# 3. 下次启动:rqt --perspective debug_layout,直接加载自定义布局。
2. 远程使用 RQT(跨机器调试)

若 ROS2 节点运行在远程机器人上,本地启动 RQT 需配置 ROS2 环境变量:

复制代码
# 本地终端配置(替换为机器人IP)
export ROS_DOMAIN_ID=相同的域ID
export ROS2_CONTROLLER_URI=http://机器人IP:8080
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp  # 或其他DDS实现
# 启动RQT,即可连接远程节点
rqt
3. RQT 插件加载失败排查
  • 检查插件是否安装:apt list | grep ros-humble-rqt-xxx
  • 重启 RQT 并查看终端报错:rqt --debug,根据报错安装缺失依赖;
  • 重置 RQT 配置:rm -rf ~/.config/rqt/,重新启动。
常见问题排查
  1. rqt_graph 看不到节点

    • 检查节点是否运行(ros2 node list);
    • 确保 ROS_DOMAIN_ID 一致(不同域 ID 的节点无法互通);
    • 点击 Refresh 刷新节点图。
  2. rqt_plot 无法添加话题

    • 话题必须是数值型(如 String 类型无法绘制曲线);
    • 话题路径填写正确(如 /chatter 是 String 类型,需填 /chatter/data);
    • 检查话题是否有数据发布(ros2 topic echo 验证)。
  3. rqt_reconfigure 看不到节点

    • 节点未实现动态参数接口(需用 declare_parameter 并设置 dynamic_typing=True);
    • 节点未运行或名称拼写错误。
总结
核心知识点
  1. RQT 是 ROS2 图形化调试框架,功能通过插件实现,替代终端命令提升调试效率;
  2. 核心插件与 ROS2 功能一一对应:rqt_graph(节点关系)、rqt_plot(数值曲线)、rqt_topic(话题调试)、rqt_reconfigure(参数调整);
  3. RQT 支持自定义布局、远程调试,是 ROS2 开发调试的必备工具。
高频工具优先级
  1. rqt_graph:优先掌握,验证节点通信是否正常;
  2. rqt_topic:一站式查看话题数据 / 频率,替代多个终端命令;
  3. rqt_plot:可视化数值数据,快速定位数据异常;
  4. rqt_console:集中查看日志,排查节点报错。
关键操作
  1. 启动方式:直接 rqt_xxx 启动单个插件,或 rqt 启动主界面;
  2. 核心逻辑:RQT 插件是终端命令的可视化封装,操作逻辑和终端命令一致;
  3. 调试流程:先用 rqt_graph 确认通信,再用 rqt_topic/rqt_plot 查看数据,最后用 rqt_reconfigure 调整参数。

9.rviz

RViz2 核心认知(先懂本质)

RViz2(ROS Visualization 2)是 ROS2 官方推出的三维可视化工具 ,核心定位是「机器人开发的可视化调试面板」,不参与机器人控制 / 计算,只负责「数据可视化展示」:

  • 🎯 核心作用:将 ROS2 中的抽象数据(如激光点云、坐标变换、路径、机器人模型)转化为直观的 3D/2D 图形;
  • 🔌 适配性:支持几乎所有 ROS2 标准数据类型(激光、图像、IMU、TF、路径等),兼容自定义数据(需配置插件);
  • 🚀 实时性:与 ROS2 话题 / 服务深度联动,数据实时刷新,调试时能即时看到效果;
  • 📌 灵活性:支持自定义视角、保存配置、加载插件,适配不同机器人场景(无人机、机械臂、移动机器人)。
RViz2 解决的核心问题

机器人开发中很多数据是「看不见摸不着」的,RViz2 能帮你快速验证逻辑是否正确:

  • 激光雷达是否正常工作?→ 在 RViz2 看激光点云是否完整;
  • TF 坐标是否配置正确?→ 在 RViz2 看各坐标系位置是否符合预期;
  • 导航路径是否合理?→ 在 RViz2 看规划的路径是否避开障碍物;
  • 无人机姿态是否正确?→ 在 RViz2 看无人机模型的俯仰 / 滚转 / 偏航是否匹配实际。
RViz2 关键知识点
1. 安装与启动(ROS2 环境)

ROS2 桌面版(ros-humble-desktop/ros-galactic-desktop)已默认安装,若未安装手动补全:

复制代码
# 安装RViz2核心包
sudo apt install ros-humble-rviz2
# 安装常用依赖(确保功能完整)
sudo apt install ros-humble-rviz-common ros-humble-rviz-default-plugins
启动方式(3 种常用)
复制代码
# 1. 基础启动(默认配置)
rviz2

# 2. 启动并加载自定义配置文件(最常用,无需重复配置)
rviz2 -d 你的配置文件路径.rviz

# 3. 从launch文件启动(推荐,和机器人节点一起启动)
# 在launch文件中添加Node配置,后续会讲
2. RViz2 核心界面组成
  1. Displays 面板(左侧,核心!):配置要显示的「数据类型」(如 TF、激光、机器人模型),所有可视化内容都需在这里添加 / 启用;
  2. 3D 可视化视图(中间,主区域):显示机器人、传感器数据、坐标系等核心内容,是调试的核心观察区;
  3. Views 面板(右侧上方):调整 3D 视图的视角(如第三人称、俯视图、跟随机器人),支持保存常用视角;
  4. Time 面板(右侧中间):控制时间同步(如实时显示 / 回放历史数据),新手默认用「Real Time」即可;
  5. 工具栏(顶部):快捷操作(视角调整、保存配置、重置视图),核心按钮要记牢。
3. 核心概念:Fixed Frame
  • 定义:RViz2 所有可视化数据的「全局参考坐标系」,所有数据都会基于这个坐标系显示位置;
  • 作用:若 Fixed Frame 配置错误,会导致「数据不显示」或「显示位置混乱」;
  • 新手推荐配置:优先选 map(全局地图坐标系)或 base_link(机器人基坐标系),根据场景切换;
  • 配置位置:Displays 面板顶部的「Fixed Frame」下拉框选择。
4. 数据展示逻辑

RViz2 本质是「话题订阅者」,所有可视化数据都来自 ROS2 话题,核心逻辑:

  1. 机器人节点发布数据到指定话题(如激光数据→/scan、TF→/tf);

  2. RViz2 在 Displays 面板添加对应「显示类型」,并订阅目标话题;

  3. RViz2 接收话题数据,转化为 3D 图形展示在主视图。

    → 核心结论:

    要显示某类数据,必须满足「机器人发布对应话题 + RViz2 配置对应显示类型」

RViz2 常用功能实操

所有功能都围绕「Displays 面板添加显示项」展开,以下是新手最常用的 8 大功能,附详细配置步骤。

前提:基础准备

启动 RViz2 后,先做 2 个基础配置(避免后续踩坑):

  1. Displays 面板顶部「Fixed Frame」:选择 base_link(先以机器人基坐标为参考);
  2. 工具栏点击「重置视角」(Reset View),让视图回到默认位置。
功能 1:显示 TF 坐标变换(必备!验证坐标系配置)

核心用途:查看所有坐标系的位置和父子关系,验证 TF 广播是否正确(如激光、相机相对于机器人的位置)

配置步骤

  1. Displays 面板点击「Add」→ 左侧列表选择「TF」→ 点击「OK」;

  2. 启用 TF 显示项(默认勾选),主视图会显示各坐标系的「坐标轴」(X 红、Y 绿、Z 蓝);

  3. 关键配置(优化显示):

    • 勾选「Show Names」:显示坐标系名称(如 base_linklaser);
    • 勾选「Show Axes」:显示坐标轴(默认勾选);
    • 调整「Axis Length」:坐标轴长度(默认 1m,按需调大 / 小);
  4. 验证:若 TF 广播正常,能看到各坐标系按实际安装位置排列(如 laserbase_link 前方 0.2m 处)。

功能 2:显示机器人模型(URDF)

核心用途:可视化机器人的 3D 模型,验证 URDF 文件是否正确(无人机、机械臂、移动机器人都适用)

配置步骤

  1. 前提:机器人节点已发布 URDF 到 /robot_description 话题(大部分机器人启动后会自动发布);

  2. Displays 面板「Add」→ 选择「RobotModel」→「OK」;

  3. 关键配置(必改):

    • 「Robot Description Topic」:默认 /robot_description(无需改,和节点发布一致即可);
    • 「Visual Enabled」:勾选(显示机器人外观);
    • 「Collision Enabled」:可选勾选(显示碰撞箱,调试避障用);
  4. 效果:主视图会显示机器人的 3D 模型,模型姿态会跟随 TF 实时变化。

功能 3:显示激光雷达数据(核心!激光调试必备)

核心用途:查看激光雷达的点云数据,验证激光是否正常、是否有障碍物

配置步骤(适配标准激光数据 sensor_msgs/msg/LaserScan

  1. Displays 面板「Add」→ 选择「LaserScan」→「OK」;

  2. 关键配置(必改!否则不显示):

    • 「Topic」:选择激光话题(通常是 /scan,按实际节点发布的话题选择);
    • 「Fixed Frame」:若全局 Fixed Frame 不对,可单独设置为 laser(激光坐标系);
  3. 优化显示:

    • 「Size (m)」:调整激光点的大小(默认 0.05m,调大更清晰);
    • 「Color」:选择激光点颜色(默认白色,可改红色);
  4. 验证:激光正常工作时,主视图会显示一圈点云,对应周围环境(有障碍物的地方有点,空旷处无点)。

功能 4:显示图像数据(相机调试必备)

核心用途:查看相机采集的图像,验证相机是否正常工作(支持单目 / 双目相机)

配置步骤(适配标准图像数据 sensor_msgs/msg/Image

  1. Displays 面板「Add」→ 选择「Image」→「OK」;

  2. 关键配置:

    • 「Topic」:选择相机话题(通常是 /camera/image_raw,按实际选择);
    • 「Image Transport」:默认「raw」(无需改);
  3. 效果:RViz2 会弹出独立窗口显示相机实时图像,也可在主视图侧边显示(调整「Position」参数)。

功能 5:显示路径(导航 / 无人机航迹必备)

核心用途:查看导航规划的路径或无人机的飞行航迹,验证路径规划是否合理

配置步骤(适配标准路径数据 nav_msgs/msg/Path

  1. Displays 面板「Add」→ 选择「Path」→「OK」;

  2. 关键配置:

    • 「Topic」:选择路径话题(如导航路径 /plan、无人机航迹 /flight_path);
    • 「Color」:设置路径颜色(推荐红色,醒目);
    • 「Line Width」:调整路径线条粗细(默认 0.02m);
  3. 效果:主视图会显示规划的路径线条,路径节点会实时更新。

功能 6:显示姿态(无人机 / 机器人姿态必备)

核心用途:查看机器人 / 无人机的姿态(俯仰、滚转、偏航),验证 IMU 数据或姿态解算是否正确

配置步骤(适配标准姿态数据 geometry_msgs/msg/PoseStamped

  1. Displays 面板「Add」→ 选择「Pose」→「OK」;

  2. 关键配置:

    • 「Topic」:选择姿态话题(如 /drone/pose);
    • 「Shape」:选择姿态显示样式(箭头 / 轴 / 球体,推荐「Arrow」);
    • 「Arrow Length」:调整箭头长度(默认 1m,适配无人机大小);
  3. 效果:箭头方向对应机器人 / 无人机的朝向,箭头姿态实时跟随话题数据变化。

功能 7:显示点云(3D 激光 / 深度相机必备)

核心用途:查看 3D 激光或深度相机的点云数据,构建环境三维模型

配置步骤(适配标准点云数据 sensor_msgs/msg/PointCloud2

  1. Displays 面板「Add」→ 选择「PointCloud2」→「OK」;

  2. 关键配置:

    • 「Topic」:选择点云话题(如 /points_raw);
    • 「Fixed Frame」:设置为 lidar(3D 激光坐标系);
    • 「Color Transformer」:选择点云着色方式(「RGB8」按颜色着色,「Intensity」按强度着色);
  3. 优化:调整「Size」让点云更清晰,勾选「Autocompute Intensity Bounds」自动适配强度范围。

功能 8:保存 / 加载配置

每次调试都要重新添加显示项?保存配置文件,下次直接加载,效率翻倍!

保存配置
  1. 配置完所有显示项后,点击顶部菜单栏「File」→「Save Config As」;
  2. 选择保存路径(建议存在功能包的 config/ 目录下),命名为 xxx.rviz(如 drone_debug.rviz);
加载配置
复制代码
# 方式1:命令行加载
rviz2 -d ~/ros2_ws/src/你的包名/config/drone_debug.rviz

# 方式2:launch文件加载(推荐,和节点一起启动)
# 后续launch示例会包含
实战配置示例:无人机调试 RViz2 配置

以「无人机 + 激光雷达」为例,提供完整的 RViz2 配置逻辑,可直接复用:

1. 核心显示项配置清单
  1. Fixed Frame:base_link(无人机基坐标系);
  2. TF:显示无人机各坐标系(base_linklaserimu);
  3. RobotModel:显示无人机 3D 模型(话题 /robot_description);
  4. LaserScan:显示激光数据(话题 /scan);
  5. Pose:显示无人机姿态(话题 /drone/pose);
  6. Path:显示飞行航迹(话题 /drone/flight_path)。
2. Launch 文件集成 RViz2

在 Launch 文件中添加 RViz2 节点,启动时自动加载配置,无需手动操作:

复制代码
# 无人机调试launch文件(drone_debug.launch.py)
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
    # 1. 获取配置文件路径
    config_dir = os.path.join(get_package_share_directory('你的包名'), 'config')
    rviz_config = os.path.join(config_dir, 'drone_debug.rviz')

    # 2. 启动RViz2节点,加载配置
    rviz2_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen',
        arguments=['-d', rviz_config]  # 加载自定义配置
    )

    # 其他节点(无人机节点、激光节点...)
    drone_node = Node(...)
    laser_node = Node(...)

    return LaunchDescription([
        drone_node,
        laser_node,
        rviz2_node  # 最后启动RViz2
    ])
常见问题排查
  1. 数据不显示?→ 先查 3 点:

    • Fixed Frame 是否配置正确(优先试 base_link);
    • 对应话题是否有数据(ros2 topic echo 话题名 验证);
    • Displays 面板中「Topic」是否选对,显示项是否勾选「Enabled」;
  2. 激光点云乱飘?→ 激光坐标系(laser)的 TF 配置错误,重新校准安装位置;

  3. 机器人模型不显示?→ 确认 /robot_description 话题有数据,RobotModel 的「Topic」配置正确;

  4. 视图看不清?→ 用工具栏调整视角(滚轮缩放、左键拖动平移、右键旋转),或重置视角。

总结
核心知识点
  1. RViz2 是「可视化调试工具」,核心是订阅 ROS2 话题并展示数据,不参与控制;
  2. Fixed Frame 是全局参考系,配置错误会导致数据不显示,新手优先用 base_link
  3. 所有可视化功能都需在「Displays 面板」添加对应显示项,并配置正确的话题。
新手必掌握的核心功能优先级
  1. TF 显示(验证坐标配置);
  2. 激光 / LaserScan 显示(传感器调试);
  3. 机器人模型 / RobotModel 显示(模型验证);
  4. 保存 / 加载配置(提升效率)。
关键操作
  1. 启动:rviz2rviz2 -d 配置文件.rviz
  2. 核心:Displays 面板「Add」添加显示项,配置「Topic」和「Fixed Frame」;
  3. 排错:先验证话题是否有数据,再检查 RViz2 配置。

10.ros2 bag

ROS2 Bag 核心认知

ROS2 Bag(简称 Bag)是 ROS2 专为话题数据设计的 "录音机 / 播放器":

  • 🎙️ 录制(record) :将指定话题的实时数据保存到二进制 Bag 文件中(后缀 .db3),包含数据内容、时间戳、消息类型等;
  • ▶️ 回放(play):从 Bag 文件中读取数据,按原时间戳(或自定义速度)重新发布到话题上,复现录制时的场景;
  • 📊 分析(info/filter):查看 Bag 文件的元信息(话题列表、数据量),或过滤提取指定数据;
  • 💡 核心价值:无需重复运行真实机器人 / 传感器,仅通过 Bag 回放就能复现问题、调试算法,大幅降低开发成本。
ROS2 Bag vs ROS1 Bag
特性 ROS2 Bag ROS1 Bag
文件格式 SQLite 数据库(.db3) 自定义格式(.bag)
录制范围 仅支持话题(Topic) 支持话题 / 服务 / 参数
DDS 兼容性 兼容所有 DDS 实现(CycloneDDS/FastDDS) 依赖 ROS Master
压缩方式 内置压缩(zstd/bz2) 需手动压缩
核心命令 ros2 bag record/play/info rosbag record/play/info
ROS2 Bag 关键知识点
1. Bag 文件的结构

ROS2 Bag 文件是一个 SQLite 数据库(.db3),包含以下核心信息:

  • 元数据:录制时间、ROS2 版本、话题列表、消息类型、数据总条数;
  • 话题数据:每条消息的内容、发布时间戳、话题名;
  • 索引:快速定位指定话题 / 时间范围的数据(回放 / 过滤时提速)。
2. 录制的核心参数
参数 作用 示例
-o/--output 指定 Bag 文件前缀(避免默认随机名) ros2 bag record -o lidar_data /scan
-a/--all 录制所有活跃话题(慎用,易产生大文件) ros2 bag record -a
-e/--regex 按正则表达式匹配话题 `ros2 bag record -e "/(scan cmd_vel)"`(录制 /scan 和 /cmd_vel)
--compression 启用压缩(降低文件大小) ros2 bag record -o data --compression zstd /chatter
--duration 指定录制时长(单位:秒) ros2 bag record --duration 60 /scan(录制 60 秒)
3. 回放的核心参数
参数 作用 示例
--rate 调整回放速度(倍数) ros2 bag play --rate 2.0 data_0.db3(2 倍速回放)
--topics 仅回放指定话题 ros2 bag play --topics /scan data_0.db3(只回放 /scan)
--start-offset 从指定时间偏移开始回放(秒) ros2 bag play --start-offset 10 data_0.db3(跳过前 10 秒)
--loop 循环回放 ros2 bag play --loop data_0.db3
--delay 话题发布前延迟(秒) ros2 bag play --delay 2 data_0.db3(延迟 2 秒开始)
4. 关键注意事项
  • 仅录制话题 :ROS2 Bag 只支持录制 / 回放话题(Topic),不支持服务(Service)、动作(Action)或参数(Parameter);
  • 时间戳对齐:回放时默认按录制的时间戳发布数据,确保和真实场景的时间节奏一致;
  • 文件大小 :高频话题(如相机 /image、激光 /scan)会快速生成大文件,建议按需录制(避免 -a);
  • 兼容性:不同 ROS2 版本录制的 Bag 文件可能不兼容(如 Humble 录制的文件无法在 Foxy 回放)。
ROS2 Bag 常用命令
1. 基础录制命令
复制代码
# 1. 录制单个话题(最常用)
ros2 bag record /chatter  # 录制/chatter,生成随机名文件(如data_2026_01_25_10_00_00)

# 2. 录制多个话题
ros2 bag record /chatter /cmd_vel /scan

# 3. 指定输出文件名(推荐,便于管理)
ros2 bag record -o my_lidar_data /scan  # 生成my_lidar_data_2026_01_25_10_00_00.db3

# 4. 录制所有话题(慎用,文件会很大)
ros2 bag record -a

# 5. 按正则表达式录制话题(灵活匹配)
ros2 bag record -e "/demo/.*"  # 录制所有以/demo/开头的话题
ros2 bag record -e "/(chatter|cmd_vel)"  # 录制/chatter和/cmd_vel

# 6. 录制并启用压缩(降低文件大小)
ros2 bag record -o compressed_data --compression zstd /scan  # zstd压缩(推荐,压缩率/速度均衡)
ros2 bag record -o compressed_data --compression bz2 /scan   # bz2压缩(压缩率更高,速度慢)

# 7. 录制指定时长(比如录制1分钟)
ros2 bag record --duration 60 /scan  # 60秒后自动停止录制
2. 查看 Bag 文件信息

录制后先查看 Bag 文件的元信息,确认数据是否正确:

复制代码
# 通用格式:ros2 bag info <Bag文件/目录>
ros2 bag info my_lidar_data_2026_01_25_10_00_00/  # Bag会生成目录,内含.db3文件

# 输出示例(关键信息标注):
# Files:             my_lidar_data_2026_01_25_10_00_00_0.db3
# Bag size:          12.3 MB
# Duration:          10.0s (录制时长)
# Start:             Jan 25 2026 10:00:00 (1740429600.000)
# End:               Jan 25 2026 10:00:10 (1740429610.000)
# Messages:          100 (总消息数)
# Topic information: Topic: /scan | Type: sensor_msgs/msg/LaserScan | Count: 100 | Serialization Format: CDR
3. 回放 Bag 文件
复制代码
# 基础回放(按原速度、发布所有话题)
ros2 bag play my_lidar_data_2026_01_25_10_00_00/

# 2倍速回放
ros2 bag play --rate 2.0 my_lidar_data_2026_01_25_10_00_00/

# 仅回放/scan话题
ros2 bag play --topics /scan my_lidar_data_2026_01_25_10_00_00/

# 跳过前5秒,从第5秒开始回放
ros2 bag play --start-offset 5 my_lidar_data_2026_01_25_10_00_00/

# 循环回放(直到手动停止)
ros2 bag play --loop my_lidar_data_2026_01_25_10_00_00/

# 回放时重映射话题名(将/scan改为/demo/scan)
ros2 bag play my_lidar_data_2026_01_25_10_00_00/ --remap /scan:=/demo/scan
4. 过滤 / 提取 Bag 数据

当 Bag 文件过大时,可过滤提取指定话题 / 时间范围的数据,生成新 Bag 文件:

复制代码
# 通用格式:ros2 bag filter <输入Bag> <输出Bag> <过滤表达式>

# 示例1:提取/scan话题,过滤掉其他话题
ros2 bag filter my_lidar_data_2026_01_25_10_00_00/ scan_only_bag "topic == '/scan'"

# 示例2:提取前5秒的所有数据
ros2 bag filter my_lidar_data_2026_01_25_10_00_00/ first_5s_bag "t.sec <= 1740429605"  # t是时间戳对象

# 示例3:提取/chat数据中内容包含"ROS2"的消息
ros2 bag filter chatter_bag/ ros2_only_bag "topic == '/chatter' and msg.data.find('ROS2') != -1"
5. 转换 Bag 文件格式(兼容 / 压缩)
复制代码
# 将 Bag 文件转换为压缩格式(降低大小)
ros2 bag convert -i my_data/ -o compressed_data/ --compression zstd

# 转换为 ROS1 Bag 格式(需安装 ros-humble-ros1-bridge)
ros2 bag convert -i my_data/ -o ros1_bag/ --output-format rosbag_v2
实操示例
步骤 1:录制话题数据
复制代码
# 1. 终端1:运行之前的话题发布节点(发布/chatter)
ros2 run demo_py_node topic_publisher

# 2. 终端2:录制/chatter话题,指定输出文件名chatter_data
ros2 bag record -o chatter_data /chatter

# 3. 录制10秒后,按 Ctrl+C 停止录制(终端会显示"Recording stopped")
步骤 2:查看 Bag 信息
复制代码
ros2 bag info chatter_data_2026_01_25_10_05_00/
# 确认输出包含:Topic: /chatter | Type: std_msgs/msg/String | Count: 10(10秒,1Hz发布)
步骤 3:回放 Bag 数据
复制代码
# 1. 终端1:运行话题订阅节点(接收/chatter)
ros2 run demo_py_node topic_subscriber

# 2. 终端2:回放Bag文件
ros2 bag play chatter_data_2026_01_25_10_05_00/

# 订阅节点终端会实时显示"收到消息:Hello ROS2! Count: x",和录制时一致
步骤 4:过滤提取数据
复制代码
# 提取/chatter中Count>5的消息,生成新Bag
ros2 bag filter chatter_data_2026_01_25_10_05_00/ count_gt5_bag "topic == '/chatter' and int(msg.data.split('Count: ')[1]) > 5"

# 回放新Bag,验证只包含Count>5的消息
ros2 bag play count_gt5_bag/
常见问题排查
  1. 录制后 Bag 文件为空 / 无数据

    • 检查录制的话题是否有数据发布(ros2 topic echo /话题名 验证);
    • 避免录制不存在的话题(ros2 topic list 确认话题活跃);
    • 录制时不要过早按 Ctrl+C(至少等待 1 秒,确保数据写入)。
  2. 回放时节点收不到数据

    • 检查回放的话题名是否和节点订阅的一致(用 ros2 topic list 确认);
    • 回放时添加 --remap 修正话题名(如 /scan 改为 /demo/scan);
    • 检查 Bag 文件是否包含该话题(ros2 bag info 验证)。
  3. Bag 文件过大

    • 避免用 -a 录制所有话题,按需指定话题名;
    • 录制时启用压缩(--compression zstd);
    • ros2 bag filter 提取关键数据,删除冗余内容。
  4. 回放速度异常(过快 / 过慢)

    • --rate 调整回放速度(如 --rate 1.0 恢复原速度);
    • 检查系统时间是否同步(Bag 依赖精确时间戳)。
总结
核心知识点
  1. ROS2 Bag 是话题数据的录制 / 回放工具,仅支持话题(不支持服务 / 动作),文件格式为 SQLite(.db3);
  2. 录制核心:按需指定话题(避免 -a)、用 -o 命名文件、启用压缩降低大小;
  3. 回放核心:支持速度调整、话题重映射、时间偏移,可精准复现录制场景;
  4. 进阶操作:用 filter 提取关键数据,减小文件体积。
高频命令
  1. 录制:ros2 bag record -o <文件名> <话题1> <话题2>
  2. 查看信息:ros2 bag info <Bag目录>
  3. 回放:ros2 bag play <Bag目录>(常用参数:--rate/--topics/--loop);
  4. 过滤:ros2 bag filter <输入> <输出> <过滤表达式>
关键使用场景
  1. 复现问题:录制故障场景的话题数据,离线回放调试;
  2. 算法调试:录制传感器数据(激光 / 相机),离线测试导航 / 感知算法;
  3. 数据分享:将 Bag 文件发给同事,无需现场即可复现场景。

11.urdf

URDF 核心认知

URDF 是基于 XML 的机器人描述格式,核心作用是将机器人拆解为 "连杆(Link)" 和 "关节(Joint)",定义:

  • 📏 几何属性:每个连杆的形状(长方体 / 圆柱 / 球体)、尺寸、外观(颜色 / 材质);
  • 📍 位置关系:关节连接的两个连杆的相对位置、旋转关系;
  • ⚖️ 物理属性:连杆的质量、惯性矩阵,关节的运动范围(限位)、阻尼等;
  • 🔌 传感器挂载:激光雷达、相机等传感器在连杆上的安装位置。
URDF 解决的核心问题

机器人是由多个部件组成的刚体系统,URDF 把 "物理机器人" 抽象为 "连杆 - 关节树",让 ROS2 能:

  1. 在 RViz2 中可视化机器人模型;
  2. 在 Gazebo 中进行物理仿真;
  3. 计算运动学 / 动力学(如逆运动学求解、碰撞检测);
  4. 定位传感器相对于机器人基坐标的位置(结合 TF2)。
URDF 关键知识点
1. URDF 的核心结构(XML 节点)

URDF 文件的核心是 <robot> 根节点,包含 <link>(连杆)和 <joint>(关节)两类子节点,结构如下:

复制代码
<?xml version="1.0"?>
<robot name="my_robot">  <!-- 机器人名称(唯一) -->
  <!-- 1. 定义连杆:base_link(机器人基连杆) -->
  <link name="base_link">
    <!-- 几何形状 -->
    <visual>
      <geometry><box size="0.2 0.1 0.1"/></geometry>  <!-- 长方体:x/y/z尺寸 -->
      <material name="blue"><color rgba="0 0 1 1"/></material>  <!-- 颜色:RGBA(透明率1) -->
    </visual>
    <!-- 碰撞检测(简化几何,提升效率) -->
    <collision>
      <geometry><box size="0.2 0.1 0.1"/></geometry>
    </collision>
    <!-- 物理属性 -->
    <inertial>
      <mass value="1.0"/>  <!-- 质量(kg) -->
      <inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.01"/>  <!-- 惯性矩阵 -->
    </inertial>
  </link>

  <!-- 2. 定义连杆:laser_link(激光雷达连杆) -->
  <link name="laser_link">
    <visual>
      <geometry><cylinder length="0.05" radius="0.02"/></geometry>  <!-- 圆柱:长度+半径 -->
      <material name="red"><color rgba="1 0 0 1"/></material>
    </visual>
  </link>

  <!-- 3. 定义关节:连接base_link和laser_link -->
  <joint name="base_to_laser" type="fixed">  <!-- fixed:固定关节(无运动) -->
    <parent link="base_link"/>  <!-- 父连杆 -->
    <child link="laser_link"/>  <!-- 子连杆 -->
    <!-- 子连杆相对于父连杆的位置(平移+旋转) -->
    <origin xyz="0.1 0 0.05" rpy="0 0 0"/>  <!-- xyz:平移,rpy:欧拉角(弧度) -->
  </joint>
</robot>
2. 核心节点详解
节点 子节点 作用
<robot> - 根节点,name 属性为机器人名称(唯一)
<link> <visual> 定义连杆的可视化外观(形状、颜色),仅用于显示,不影响物理仿真
<collision> 定义连杆的碰撞几何(简化形状),用于碰撞检测 / 仿真
<inertial> 定义连杆的质量、惯性矩阵,用于动力学仿真(Gazebo 必需)
<joint> <parent>/<child> 定义关节连接的父 / 子连杆(必须存在)
<origin> 子连杆相对于父连杆的位姿(xyz:平移,rpy:旋转)
<limit> 运动关节的限位(如旋转角度范围、速度限制)
3. 关节类型
关节类型 特点 适用场景
fixed 固定关节,无运动 传感器 / 固定部件(如激光雷达安装)
revolute 旋转关节,单轴旋转 车轮、机械臂关节(有角度限位)
prismatic 移动关节,单轴平移 线性滑轨、升降台
continuous 连续旋转关节,无角度限位 电机轴、无刷电机
4. URDF 与 TF2 的关系

URDF 定义的 "连杆 - 关节树" 会自动生成 TF 变换树:

  • 每个 <link> 对应一个 TF 坐标系;
  • 每个 <joint><origin> 定义了父子连杆的 TF 变换;
  • 启动机器人状态发布器(robot_state_publisher)后,会自动广播 URDF 中的 TF 变换,无需手动编写 TF 广播代码。
5. URDF 的局限性与替代方案
  • 局限性:XML 格式繁琐,复杂机器人编写易出错;不支持模块化复用(如重复连杆需重复定义);

  • 替代方案

    • Xacro:URDF 的升级版(XML 宏),支持变量、宏定义、条件判断,大幅简化复杂机器人描述(新手推荐);
    • SDF:Gazebo 原生格式,支持更丰富的物理属性(如摩擦力、弹性),可与 URDF 转换。
URDF 常用命令
1. 检查 URDF 语法正确性

编写 URDF 后首先验证语法,避免可视化 / 仿真时报错:

复制代码
# 通用格式:检查URDF文件语法
check_urdf <urdf文件路径>

# 示例:检查my_robot.urdf
check_urdf my_robot.urdf

# 输出说明:
# - 若语法正确:显示"robot name is: my_robot"+连杆/关节列表;
# - 若语法错误:提示具体错误位置(如缺少</link>标签)。
2. 生成 URDF 变换树(可视化连杆 - 关节关系)
复制代码
# 通用格式:生成URDF的变换树(.dot文件)
urdf_to_graphiz <urdf文件路径>

# 示例:生成my_robot.urdf的变换树
urdf_to_graphiz my_robot.urdf

# 生成my_robot.pdf文件,查看连杆-关节树:
evince my_robot.pdf
3. 在 RViz2 中可视化 URDF

需结合 robot_state_publisher 发布 URDF 模型,步骤:

复制代码
# 1. 启动RViz2+机器人状态发布器(加载URDF文件)
ros2 launch urdf_tutorial display.launch.py model:=/path/to/my_robot.urdf

# 2. 或手动启动(分步):
# 终端1:启动机器人状态发布器(发布URDF和TF)
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat /path/to/my_robot.urdf)"

# 终端2:启动RViz2
rviz2

# RViz2配置:
# - Fixed Frame 设置为 base_link;
# - Displays → Add → RobotModel;
# - RobotModel 的 Robot Description 填写 /robot_description;
# 即可看到机器人模型可视化效果。
4. 将 URDF 转换为 SDF(Gazebo 仿真)

Gazebo 优先支持 SDF 格式,可将 URDF 转换为 SDF:

复制代码
# 通用格式:URDF转SDF
gz sdf -p <urdf文件路径> > <输出sdf文件>

# 示例:
gz sdf -p my_robot.urdf > my_robot.sdf
5. Xacro 编译为 URDF(新手推荐)

Xacro 是 URDF 的简化版,需先编译为 URDF 才能使用:

复制代码
# 通用格式:Xacro编译为URDF
xacro <xacro文件路径> > <输出urdf文件>

# 示例:
xacro my_robot.xacro > my_robot.urdf
实操示例
步骤 1:编写简单 URDF 文件

创建 my_robot.urdf 文件,内容为上文的核心结构示例(base_link + laser_link + fixed 关节)。

步骤 2:验证 URDF 语法
复制代码
check_urdf my_robot.urdf
# 输出应包含:
# robot name is: my_robot
# -------- Successfully Parsed XML ---------------
# Link base_link:
#     Links: 
#         base_link (parent) -> laser_link (child)
#     Joints: base_to_laser
步骤 3:生成变换树
复制代码
urdf_to_graphiz my_robot.urdf
evince my_robot.pdf
# 能看到 base_link → laser_link 的固定关节关系。
步骤 4:RViz2 可视化
复制代码
# 启动机器人状态发布器
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat my_robot.urdf)"

# 启动RViz2,配置后能看到:
# - 蓝色长方体(base_link);
# - 红色圆柱(laser_link)在base_link前方0.1m、上方0.05m处。
常见问题排查
  1. RViz2 中看不到机器人模型

    • 检查 Fixed Frame 是否设置为 URDF 中的根连杆(如 base_link);
    • 检查 robot_state_publisher 是否正确加载 URDF(ros2 param get /robot_state_publisher robot_description 验证);
    • 检查 URDF 语法(check_urdf),确保无语法错误。
  2. check_urdf 报错 "Invalid XML"

    • 检查 XML 标签是否闭合(如 <link> 必须有 </link>);
    • 检查属性值是否合法(如 xyz 是 3 个数字,用空格分隔);
    • 避免中文 / 特殊字符(URDF 仅支持 ASCII 字符)。
  3. Gazebo 仿真中机器人 "飘起来"

    • 缺少 <inertial> 节点(Gazebo 必需质量 / 惯性矩阵);
    • 惯性矩阵设置不合理(如质量为 0,或惯性值过大 / 过小)。
  4. 关节运动异常(如旋转超出限位)

    • 检查 <joint><limit> 节点(如 lower/upper 定义角度范围);
    • 关节类型错误(如需旋转用 revolute,而非 fixed)。
总结
核心知识点
  1. URDF 是 ROS2 描述机器人的 XML 格式,核心是 <link>(连杆)和 <joint>(关节),定义机器人的几何 / 物理 / 位姿属性;
  2. 关节类型中 fixed(固定)最常用(传感器安装),revolute(旋转)用于运动关节;
  3. URDF 会自动生成 TF 变换树,通过 robot_state_publisher 广播 TF 变换;
  4. Xacro 是 URDF 的简化版,支持变量 / 宏,大幅降低编写复杂度(新手优先用 Xacro)。
高频命令
  1. 语法检查:check_urdf <urdf文件>(编写后第一步);
  2. 变换树生成:urdf_to_graphiz <urdf文件>(可视化连杆 - 关节关系);
  3. 可视化:ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat <urdf文件>)" + RViz2;
  4. Xacro 编译:xacro <xacro文件> > <urdf文件>
关键使用流程
  1. 编写 URDF/Xacro 文件 → 2. check_urdf 验证语法 → 3. urdf_to_graphiz 查看变换树 → 4. RViz2 可视化调试 → 5. 转换为 SDF 用于 Gazebo 仿真

12.xacro

Xacro 核心认知
1. 什么是 Xacro?

Xacro(XML Macros)是为 URDF 设计的预处理工具,本质是 "带语法增强的 XML":

  • ✅ 支持变量定义 :用变量替代硬编码的尺寸 / 质量(如 <xacro:property name="base_length" value="0.2"/>);
  • ✅ 支持宏定义:复用重复的连杆 / 关节(如多个车轮只需定义一次宏);
  • ✅ 支持数学运算 :直接计算尺寸(如 $(arg wheel_radius) * 2);
  • ✅ 支持条件判断:根据参数生成不同模型(如是否加载传感器);
  • ✅ 最终输出:Xacro 文件需编译为标准 URDF 文件,才能被 ROS2 识别(xacro xxx.xacro > xxx.urdf)。
2. Xacro 与 URDF 的核心区别
特性 URDF Xacro
变量 / 参数 无,硬编码所有数值 支持变量、参数、数学运算
代码复用 无,重复连杆需重复写 宏定义(macro)一键复用
逻辑控制 支持条件判断、循环(进阶)
文件体积 大(冗余代码多) 小(模块化、复用性强)
使用方式 直接使用 先编译为 URDF 再使用
3. Xacro 的核心价值
  • 简化编写:复杂机器人(如多轮小车、机械臂)的代码量减少 50% 以上;
  • 易于维护:修改尺寸 / 参数只需改变量,无需逐行修改所有硬编码值;
  • 模块化 :可拆分多个 Xacro 文件(如 base.xacrolaser.xacro),通过 <xacro:include> 组合,降低耦合。
Xacro 核心语法

Xacro 基于 XML 扩展,核心语法都以 xacro: 前缀标识,以下是新手最常用的 5 个核心特性:

1. 变量定义(property)
作用

定义可复用的变量(如尺寸、质量、颜色),替代 URDF 中的硬编码数值,便于统一修改。

语法
复制代码
<!-- 基础变量定义:name=变量名,value=值(数值/字符串/列表) -->
<xacro:property name="base_length" value="0.2"/>    <!-- 数值 -->
<xacro:property name="base_width" value="0.1"/>
<xacro:property name="base_height" value="0.1"/>
<xacro:property name="base_color" value="0 0 1 1"/> <!-- RGBA颜色 -->

<!-- 数学运算:用${}包裹表达式,支持+、-、*、/、() -->
<xacro:property name="base_volume" value="${base_length * base_width * base_height}"/>
使用方式
复制代码
<!-- 在URDF节点中引用变量:${变量名} -->
<link name="base_link">
  <visual>
    <geometry><box size="${base_length} ${base_width} ${base_height}"/></geometry>
    <material name="blue"><color rgba="${base_color}"/></material>
  </visual>
</link>
2. 宏定义(macro)
作用

定义可复用的 "代码块"(如重复的连杆、关节),只需写一次,多次调用(核心!)。

语法
复制代码
<!-- 定义宏:name=宏名,params=参数列表(多个参数用空格分隔) -->
<xacro:macro name="wheel_link" params="wheel_name wheel_radius wheel_mass">
  <link name="${wheel_name}">
    <visual>
      <geometry><cylinder length="0.05" radius="${wheel_radius}"/></geometry>
      <material name="gray"><color rgba="0.5 0.5 0.5 1"/></material>
    </visual>
    <inertial>
      <mass value="${wheel_mass}"/>
      <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
    </inertial>
  </link>
</xacro:macro>

<!-- 调用宏:传入参数(顺序/键值对均可) -->
<xacro:wheel_link wheel_name="left_wheel" wheel_radius="0.05" wheel_mass="0.2"/>
<xacro:wheel_link wheel_name="right_wheel" wheel_radius="0.05" wheel_mass="0.2"/>
3. 文件包含(include)
作用

拆分大型 Xacro 文件为多个模块化文件(如底盘、传感器、机械臂),便于维护和复用。

语法
复制代码
<!-- 1. 包含同目录下的Xacro文件 -->
<xacro:include filename="base.xacro"/>
<xacro:include filename="laser.xacro"/>

<!-- 2. 包含其他目录的Xacro文件(绝对/相对路径) -->
<xacro:include filename="$(find my_robot_description)/urdf/sensors/imu.xacro"/>

<!-- 3. 包含时传递参数(给被包含文件的变量赋值) -->
<xacro:include filename="laser.xacro">
  <xacro:arg name="laser_x" value="0.1"/>  <!-- 激光雷达x轴偏移 -->
  <xacro:arg name="laser_z" value="0.05"/> <!-- 激光雷达z轴偏移 -->
</xacro:include>
4. 参数传递(arg)
作用

定义 "外部可配置的参数",编译 Xacro 时可通过命令行传入,实现 "一份模型,多套配置"。

语法
复制代码
<!-- 1. 定义参数(可指定默认值) -->
<xacro:arg name="robot_name" default="my_robot"/>
<xacro:arg name="has_laser" default="true"/> <!-- 布尔值:是否加载激光雷达 -->

<!-- 2. 引用参数(和变量用法一致:${参数名}) -->
<robot name="${robot_name}">
  <!-- 3. 条件判断:根据参数决定是否加载激光雷达(进阶) -->
  <xacro:if value="${has_laser}">
    <xacro:include filename="laser.xacro"/>
  </xacro:if>
</robot>
编译时传参
复制代码
# 编译时覆盖默认参数:不加载激光雷达,机器人名改为"small_robot"
xacro my_robot.xacro robot_name:=small_robot has_laser:=false > my_robot.urdf
5. 数学运算与函数
核心运算

Xacro 支持基本数学运算和常用函数,直接在 ${} 中使用:

复制代码
<!-- 基础运算:+ - * / () -->
<xacro:property name="wheel_diameter" value="${wheel_radius * 2}"/>
<xacro:property name="half_length" value="${base_length / 2}"/>

<!-- 常用函数(ROS2 内置) -->
<xacro:property name="pi" value="${3.1415926}"/>
<xacro:property name="angle_rad" value="${deg2rad(90)}"/> <!-- 角度转弧度 -->
<xacro:property name="angle_deg" value="${rad2deg(1.57)}"/> <!-- 弧度转角度 -->
<xacro:property name="abs_value" value="${abs(-0.1)}"/>     <!-- 绝对值 -->
<xacro:property name="max_value" value="${max(0.1, 0.2)}"/> <!-- 最大值 -->
Xacro 实操示例
示例:编写两轮小车的 Xacro 文件

创建 two_wheeled_robot.xacro,包含变量、宏、参数、条件判断:

复制代码
<?xml version="1.0"?>
<robot name="two_wheeled_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 1. 定义全局变量 -->
  <xacro:property name="base_length" value="0.2"/>
  <xacro:property name="base_width" value="0.15"/>
  <xacro:property name="base_height" value="0.1"/>
  <xacro:property name="wheel_radius" value="0.05"/>
  <xacro:property name="wheel_width" value="0.03"/>
  <xacro:property name="wheel_mass" value="0.2"/>

  <!-- 2. 定义参数:是否加载激光雷达(默认加载) -->
  <xacro:arg name="has_laser" default="true"/>

  <!-- 3. 定义宏:车轮连杆+关节(复用两次) -->
  <xacro:macro name="wheel" params="wheel_name parent_link x_offset y_offset">
    <!-- 车轮连杆 -->
    <link name="${wheel_name}">
      <visual>
        <geometry><cylinder length="${wheel_width}" radius="${wheel_radius}"/></geometry>
        <material name="gray"><color rgba="0.5 0.5 0.5 1"/></material>
      </visual>
      <inertial>
        <mass value="${wheel_mass}"/>
        <inertia ixx="0.001" ixy="0.0" ixz="0.0" iyy="0.001" iyz="0.0" izz="0.001"/>
      </inertial>
    </link>
    <!-- 车轮关节(连续旋转关节) -->
    <joint name="${parent_link}_to_${wheel_name}" type="continuous">
      <parent link="${parent_link}"/>
      <child link="${wheel_name}"/>
      <origin xyz="${x_offset} ${y_offset} -${base_height/2}" rpy="${deg2rad(90)} 0 0"/>
      <axis xyz="0 1 0"/> <!-- 旋转轴:y轴 -->
    </joint>
  </xacro:macro>

  <!-- 4. 定义底盘连杆 -->
  <link name="base_link">
    <visual>
      <geometry><box size="${base_length} ${base_width} ${base_height}"/></geometry>
      <material name="blue"><color rgba="0 0 1 1"/></material>
    </visual>
    <inertial>
      <mass value="1.0"/>
      <inertia ixx="0.01" ixy="0.0" ixz="0.0" iyy="0.01" iyz="0.0" izz="0.01"/>
    </inertial>
  </link>

  <!-- 5. 调用宏:创建左右车轮 -->
  <xacro:wheel wheel_name="left_wheel" parent_link="base_link" x_offset="0" y_offset="${base_width/2 + wheel_width/2}"/>
  <xacro:wheel wheel_name="right_wheel" parent_link="base_link" x_offset="0" y_offset="-${base_width/2 - wheel_width/2}"/>

  <!-- 6. 条件判断:加载激光雷达 -->
  <xacro:if value="${has_laser}">
    <link name="laser_link">
      <visual>
        <geometry><cylinder length="0.05" radius="0.02"/></geometry>
        <material name="red"><color rgba="1 0 0 1"/></material>
      </visual>
    </link>
    <joint name="base_to_laser" type="fixed">
      <parent link="base_link"/>
      <child link="laser_link"/>
      <origin xyz="${base_length/2} 0 ${base_height/2}" rpy="0 0 0"/>
    </joint>
  </xacro:if>
</robot>
编译与验证
复制代码
# 1. 编译Xacro为URDF(使用默认参数:加载激光雷达)
xacro two_wheeled_robot.xacro > two_wheeled_robot.urdf

# 2. 编译时传参:不加载激光雷达
xacro two_wheeled_robot.xacro has_laser:=false > two_wheeled_robot_no_laser.urdf

# 3. 验证URDF语法(确保无错误)
check_urdf two_wheeled_robot.urdf

# 4. RViz2可视化
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(cat two_wheeled_robot.urdf)"
rviz2  # 配置Fixed Frame=base_link,添加RobotModel查看
Xacro 常用技巧
1. 模块化拆分(大型模型必备)

将复杂模型拆分为多个 Xacro 文件,通过 <xacro:include> 组合,便于维护:

复制代码
# 目录结构
my_robot/
├── urdf/
│   ├── base.xacro       # 底盘
│   ├── wheels.xacro     # 车轮
│   ├── sensors/
│   │   ├── laser.xacro  # 激光雷达
│   │   └── camera.xacro # 相机
│   └── my_robot.xacro   # 主文件(包含所有子文件)

主文件 my_robot.xacro

复制代码
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <xacro:include filename="base.xacro"/>
  <xacro:include filename="wheels.xacro"/>
  <xacro:include filename="sensors/laser.xacro"/>
  <xacro:include filename="sensors/camera.xacro"/>
</robot>
2. 循环生成重复部件(进阶)

<xacro:for> 循环生成多个相同部件(如 6 轮小车的车轮):

复制代码
<!-- 循环生成6个车轮(xacro 1.12+支持) -->
<xacro:property name="wheel_positions" value="[-0.1, -0.05, 0, 0.05, 0.1, 0.15]"/>
<xacro:for item="y_offset" in="${wheel_positions}">
  <xacro:wheel wheel_name="wheel_${loop.index}" parent_link="base_link" x_offset="0" y_offset="${y_offset}"/>
</xacro:for>
3. 与 Launch 文件结合(自动编译)

无需手动编译 Xacro,在 Launch 文件中直接加载 Xacro 并传递参数:

复制代码
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch.actions import DeclareLaunchArgument
import xacro
import os

def generate_launch_description():
    # 1. 声明参数
    has_laser_arg = DeclareLaunchArgument(
        'has_laser',
        default_value='true',
        description='是否加载激光雷达'
    )

    # 2. 加载Xacro文件并编译
    xacro_path = os.path.join('demo_py_node', 'urdf', 'two_wheeled_robot.xacro')
    robot_description_config = xacro.process_file(
        xacro_path,
        mappings={'has_laser': LaunchConfiguration('has_laser')}  # 传递参数
    )
    robot_description = {'robot_description': robot_description_config.toxml()}

    # 3. 启动机器人状态发布器
    robot_state_pub_node = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[robot_description],
        output='screen'
    )

    return LaunchDescription([
        has_laser_arg,
        robot_state_pub_node
    ])

运行 Launch 文件(传参):

复制代码
ros2 launch demo_py_node robot_launch.py has_laser:=false
4. 常用调试命令
复制代码
# 1. 查看Xacro编译后的URDF内容(调试用)
xacro two_wheeled_robot.xacro --inorder

# 2. 检查Xacro语法(ROS2 Humble+支持)
xacro --check two_wheeled_robot.xacro

# 3. 编译时输出调试信息
xacro two_wheeled_robot.xacro --verbose > two_wheeled_robot.urdf
常见问题排查
  1. 编译报错 "undefined variable/arg"
  • 检查变量 / 参数名是否拼写错误(区分大小写,如 base_lengthBase_Length);
  • 确认变量 / 参数在使用前已定义(Xacro 按顺序解析,先定义后使用);
  • 包含文件时,确认参数已传递(如 <xacro:arg> 需在 <xacro:include> 中赋值)。
  1. 数学运算报错 "invalid expression"
  • 确保运算表达式在 ${} 内(如 ${a + b},而非 $a + b);
  • 避免除以 0、非法函数调用(如 deg2rad("90") 字符串转角度,需用数值 deg2rad(90));
  • 复杂运算拆分(如 ${(a + b) * c} 加括号明确优先级)。
  1. RViz2 中模型显示异常
  • 编译后的 URDF 语法错误(先用 check_urdf 验证);
  • 宏参数传递错误(如车轮偏移量计算错误导致位置异常);
  • 缺少 xmlns:xacro 命名空间(根节点必须加 xmlns:xacro="http://www.ros.org/wiki/xacro")。
  1. 条件判断不生效
  • 布尔值需用 ${true}/${false},而非 true/false(如 <xacro:if value="${has_laser}">);
  • 确认参数传递正确(编译时传参需用 :=,而非 =)。
总结
核心知识点
  1. Xacro 是 URDF 的增强版,核心特性:变量(property)、宏(macro)、参数(arg)、数学运算、条件判断
  2. Xacro 需编译为 URDF 才能使用,编译命令:xacro xxx.xacro [参数名:=值] > xxx.urdf
  3. 模块化拆分是编写复杂模型的核心技巧,通过 <xacro:include> 组合子文件;
  4. 根节点必须添加 Xacro 命名空间:xmlns:xacro="http://www.ros.org/wiki/xacro"
必记语法清单
语法 作用 示例
<xacro:property> 定义变量 <xacro:property name="r" value="0.05"/>
<xacro:macro> 定义复用宏 <xacro:macro name="wheel" params="name r"/>
<xacro:arg> 定义外部参数 <xacro:arg name="has_laser" default="true"/>
<xacro:include> 包含其他 Xacro 文件 <xacro:include filename="laser.xacro"/>
${} 引用变量 / 运算 ${r * 2}${deg2rad(90)}
<xacro:if> 条件判断 <xacro:if value="${has_laser}">
关键使用流程
  1. 拆分模块化 Xacro 文件 → 2. 定义变量 / 宏 / 参数 → 3. 编译为 URDF 并验证语法 → 4. Launch 文件自动加载(可选)→ 5. RViz2 可视化调试。
常用模板
0.通用前置模板(所有文件必加)

所有 Xacro 文件开头需添加以下内容,定义通用变量和命名空间:

复制代码
<?xml version="1.0"?>
<robot name="robot_modules" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 通用数学常量 -->
  <xacro:property name="pi" value="${3.1415926}"/>
  <!-- 通用颜色定义 -->
  <xacro:property name="color_blue" value="0 0 1 1"/>
  <xacro:property name="color_red" value="1 0 0 1"/>
  <xacro:property name="color_gray" value="0.5 0.5 0.5 1"/>
  <xacro:property name="color_black" value="0 0 0 1"/>
  <xacro:property name="color_white" value="1 1 1 1"/>

  <!-- ========== 以下添加具体模块模板 ========== -->

</robot>
1. 基础底盘模板(Box 型)

适用于轮式小车、移动机器人的基础底盘,支持自定义尺寸 / 质量:

复制代码
<!-- 宏:基础底盘连杆 -->
<xacro:macro name="base_link" params="name length width height mass color">
  <link name="${name}">
    <!-- 可视化属性 -->
    <visual>
      <geometry>
        <box size="${length} ${width} ${height}"/>
      </geometry>
      <material name="base_material">
        <color rgba="${color}"/>
      </material>
      <!-- 底盘原点在几何中心 -->
      <origin xyz="0 0 0" rpy="0 0 0"/>
    </visual>

    <!-- 碰撞属性(与可视化一致) -->
    <collision>
      <geometry>
        <box size="${length} ${width} ${height}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="0 0 0"/>
    </collision>

    <!-- 惯性属性(简化版,适用于规则立方体) -->
    <inertial>
      <mass value="${mass}"/>
      <inertia 
        ixx="${mass * (width*width + height*height)/12}"
        ixy="0.0"
        ixz="0.0"
        iyy="${mass * (length*length + height*height)/12}"
        iyz="0.0"
        izz="${mass * (length*length + width*width)/12}"
      />
    </inertial>
  </link>
</xacro:macro>

<!-- 调用示例:创建base_link底盘 -->
<xacro:base_link 
  name="base_link" 
  length="0.3"    <!-- x轴长度(m) -->
  width="0.2"     <!-- y轴宽度(m) -->
  height="0.1"    <!-- z轴高度(m) -->
  mass="1.5"      <!-- 质量(kg) -->
  color="${color_blue}"
/>
2. 车轮模板(圆柱型,支持连续旋转)

适用于差速轮、阿克曼轮,包含连杆 + 关节,支持自定义安装位置:

复制代码
<!-- 宏:车轮(连杆+关节) -->
<xacro:macro name="wheel" params="wheel_name parent_link x_offset y_offset z_offset radius width mass">
  <!-- 1. 车轮连杆 -->
  <link name="${wheel_name}">
    <visual>
      <geometry>
        <cylinder length="${width}" radius="${radius}"/>
      </geometry>
      <material name="wheel_material">
        <color rgba="${color_gray}"/>
      </material>
      <!-- 车轮旋转轴对齐y轴 -->
      <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
    </visual>

    <collision>
      <geometry>
        <cylinder length="${width}" radius="${radius}"/>
      </geometry>
      <origin xyz="0 0 0" rpy="${pi/2} 0 0"/>
    </collision>

    <inertial>
      <mass value="${mass}"/>
      <!-- 圆柱惯性矩阵(简化版) -->
      <inertia 
        ixx="${mass*(3*radius*radius + width*width)/12}"
        ixy="0.0"
        ixz="0.0"
        iyy="${mass*(3*radius*radius + width*width)/12}"
        iyz="0.0"
        izz="${mass*radius*radius/2}"
      />
    </inertial>
  </link>

  <!-- 2. 车轮关节(连续旋转,无角度限位) -->
  <joint name="${parent_link}_to_${wheel_name}" type="continuous">
    <parent link="${parent_link}"/>
    <child link="${wheel_name}"/>
    <!-- 车轮安装位置(相对于父连杆) -->
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="0 0 0"/>
    <!-- 旋转轴:y轴(车轮绕y轴旋转) -->
    <axis xyz="0 1 0"/>
    <!-- 关节阻尼(仿真用) -->
    <dynamics damping="0.01" friction="0.01"/>
  </joint>
</xacro:macro>

<!-- 调用示例:创建左右车轮 -->
<!-- 左车轮:y轴正方向偏移 -->
<xacro:wheel 
  wheel_name="left_wheel" 
  parent_link="base_link" 
  x_offset="0"          <!-- x轴偏移 -->
  y_offset="0.1"        <!-- y轴偏移(底盘宽度/2) -->
  z_offset="-0.05"      <!-- z轴偏移(底盘高度/2,接地) -->
  radius="0.06"         <!-- 车轮半径(m) -->
  width="0.03"          <!-- 车轮宽度(m) -->
  mass="0.3"            <!-- 车轮质量(kg) -->
/>

<!-- 右车轮:y轴负方向偏移 -->
<xacro:wheel 
  wheel_name="right_wheel" 
  parent_link="base_link" 
  x_offset="0"          
  y_offset="-0.1"       
  z_offset="-0.05"      
  radius="0.06"         
  width="0.03"          
  mass="0.3"            
/>
3. 激光雷达模板(2D/3D LiDAR)

适用于常见激光雷达(如思岚 A1、速腾 16 线),固定关节安装:

复制代码
<!-- 宏:激光雷达 -->
<xacro:macro name="lidar" params="name parent_link x_offset y_offset z_offset roll pitch yaw">
  <!-- 1. 激光雷达连杆 -->
  <link name="${name}">
    <visual>
      <geometry>
        <cylinder length="0.08" radius="0.04"/>
      </geometry>
      <material name="lidar_material">
        <color rgba="${color_red}"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <cylinder length="0.08" radius="0.04"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.2"/>
      <inertia 
        ixx="0.0001" ixy="0.0" ixz="0.0"
        iyy="0.0001" iyz="0.0"
        izz="0.0001"
      />
    </inertial>
  </link>

  <!-- 2. 激光雷达关节(固定) -->
  <joint name="${parent_link}_to_${name}" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="${roll} ${pitch} ${yaw}"/>
  </joint>
</xacro:macro>

<!-- 调用示例:安装思岚A1激光雷达 -->
<xacro:lidar 
  name="laser_link" 
  parent_link="base_link" 
  x_offset="0.15"    <!-- 底盘前方0.15m -->
  y_offset="0"       <!-- 中心位置 -->
  z_offset="0.08"    <!-- 离地0.08m -->
  roll="0" 
  pitch="0" 
  yaw="0"
/>
4. 相机模板(RGB-D / 单目)

适用于 USB 相机、深度相机(如 Intel Realsense),支持自定义安装角度:

复制代码
<!-- 宏:相机 -->
<xacro:macro name="camera" params="name parent_link x_offset y_offset z_offset roll pitch yaw">
  <!-- 1. 相机连杆 -->
  <link name="${name}">
    <visual>
      <geometry>
        <box size="0.04 0.06 0.03"/>
      </geometry>
      <material name="camera_material">
        <color rgba="${color_black}"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <box size="0.04 0.06 0.03"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.1"/>
      <inertia 
        ixx="0.00005" ixy="0.0" ixz="0.0"
        iyy="0.00005" iyz="0.0"
        izz="0.00005"
      />
    </inertial>
  </link>

  <!-- 2. 相机关节(固定) -->
  <joint name="${parent_link}_to_${name}" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="${roll} ${pitch} ${yaw}"/>
  </joint>
</xacro:macro>

<!-- 调用示例:安装前视相机(向下倾斜10度) -->
<xacro:camera 
  name="camera_link" 
  parent_link="base_link" 
  x_offset="0.18"        <!-- 底盘前方0.18m -->
  y_offset="0"           
  z_offset="0.1"         <!-- 离地0.1m -->
  roll="0" 
  pitch="${deg2rad(-10)}"<!-- 向下倾斜10度 -->
  yaw="0"
/>
5. IMU 模板(惯性测量单元)

适用于常见 IMU(如 MPU6050、BMI088),安装在机器人重心位置:

复制代码
<!-- 宏:IMU -->
<xacro:macro name="imu" params="name parent_link x_offset y_offset z_offset roll pitch yaw">
  <!-- 1. IMU连杆 -->
  <link name="${name}">
    <visual>
      <geometry>
        <box size="0.02 0.02 0.01"/>
      </geometry>
      <material name="imu_material">
        <color rgba="${color_white}"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <box size="0.02 0.02 0.01"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.01"/>
      <inertia 
        ixx="0.000001" ixy="0.0" ixz="0.0"
        iyy="0.000001" iyz="0.0"
        izz="0.000001"
      />
    </inertial>
  </link>

  <!-- 2. IMU关节(固定) -->
  <joint name="${parent_link}_to_${name}" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="${roll} ${pitch} ${yaw}"/>
  </joint>
</xacro:macro>

<!-- 调用示例:安装IMU在底盘中心 -->
<xacro:imu 
  name="imu_link" 
  parent_link="base_link" 
  x_offset="0"       <!-- 中心位置 -->
  y_offset="0"       
  z_offset="0.05"    <!-- 底盘顶部中心 -->
  roll="0" 
  pitch="0" 
  yaw="0"
/>
6. 机械臂单关节模板(旋转关节)

适用于 6 轴 / 7 轴机械臂的旋转关节,支持角度限位:

复制代码
<!-- 宏:机械臂旋转关节 -->
<xacro:macro name="arm_joint" params="joint_name link_name parent_link x_offset y_offset z_offset roll pitch yaw lower_limit upper_limit max_velocity max_effort">
  <!-- 1. 关节连杆 -->
  <link name="${link_name}">
    <visual>
      <geometry>
        <cylinder length="0.15" radius="0.03"/>
      </geometry>
      <material name="arm_material">
        <color rgba="${color_gray}"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <cylinder length="0.15" radius="0.03"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.5"/>
      <inertia 
        ixx="0.001" ixy="0.0" ixz="0.0"
        iyy="0.001" iyz="0.0"
        izz="0.001"
      />
    </inertial>
  </link>

  <!-- 2. 旋转关节(带角度限位) -->
  <joint name="${joint_name}" type="revolute">
    <parent link="${parent_link}"/>
    <child link="${link_name}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="${roll} ${pitch} ${yaw}"/>
    <axis xyz="0 0 1"/> <!-- 绕z轴旋转 -->
    <!-- 角度限位 -->
    <limit 
      lower="${lower_limit}" 
      upper="${upper_limit}" 
      velocity="${max_velocity}" 
      effort="${max_effort}"
    />
    <dynamics damping="0.02" friction="0.02"/>
  </joint>
</xacro:macro>

<!-- 调用示例:创建机械臂第一关节 -->
<xacro:arm_joint 
  joint_name="joint1" 
  link_name="link1" 
  parent_link="base_link" 
  x_offset="0" 
  y_offset="0" 
  z_offset="0.1" 
  roll="0" 
  pitch="0" 
  yaw="0" 
  lower_limit="${deg2rad(-180)}"  <!-- 下限-180度 -->
  upper_limit="${deg2rad(180)}"   <!-- 上限180度 -->
  max_velocity="1.0"              <!-- 最大角速度(rad/s) -->
  max_effort="5.0"                <!-- 最大力矩(N·m) -->
/>
7. 脚轮模板(万向轮 / 从动轮)

适用于三轮 / 四轮小车的从动脚轮,固定关节无旋转:

复制代码
<!-- 宏:脚轮 -->
<xacro:macro name="caster" params="name parent_link x_offset y_offset z_offset">
  <!-- 1. 脚轮连杆 -->
  <link name="${name}">
    <visual>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
      <material name="caster_material">
        <color rgba="${color_black}"/>
      </material>
    </visual>

    <collision>
      <geometry>
        <sphere radius="0.03"/>
      </geometry>
    </collision>

    <inertial>
      <mass value="0.1"/>
      <inertia 
        ixx="0.00005" ixy="0.0" ixz="0.0"
        iyy="0.00005" iyz="0.0"
        izz="0.00005"
      />
    </inertial>
  </link>

  <!-- 2. 脚轮关节(固定) -->
  <joint name="${parent_link}_to_${name}" type="fixed">
    <parent link="${parent_link}"/>
    <child link="${name}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="0 0 0"/>
  </joint>
</xacro:macro>

<!-- 调用示例:安装后脚轮 -->
<xacro:caster 
  name="caster_link" 
  parent_link="base_link" 
  x_offset="-0.15"   <!-- 底盘后方0.15m -->
  y_offset="0"       
  z_offset="-0.03"   <!-- 接地,略高于主动轮 -->
/>
8. 云台模板(两轴旋转)

适用于相机 / 激光雷达云台,支持俯仰 + 偏航旋转:

复制代码
<!-- 宏:云台偏航关节(绕z轴) -->
<xacro:macro name="pan_joint" params="name parent_link child_link x_offset y_offset z_offset">
  <joint name="${name}" type="continuous">
    <parent link="${parent_link}"/>
    <child link="${child_link}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <dynamics damping="0.01" friction="0.01"/>
  </joint>
</xacro:macro>

<!-- 宏:云台俯仰关节(绕y轴) -->
<xacro:macro name="tilt_joint" params="name parent_link child_link x_offset y_offset z_offset lower upper">
  <joint name="${name}" type="revolute">
    <parent link="${parent_link}"/>
    <child link="${child_link}"/>
    <origin xyz="${x_offset} ${y_offset} ${z_offset}" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
    <limit lower="${lower}" upper="${upper}" velocity="0.5" effort="2.0"/>
    <dynamics damping="0.01" friction="0.01"/>
  </joint>
</xacro:macro>

<!-- 调用示例:创建相机云台 -->
<!-- 1. 云台底座 -->
<link name="pan_link">
  <visual><geometry><cylinder length="0.05" radius="0.04"/></geometry><material name="gray"><color rgba="${color_gray}"/></material></visual>
  <collision><geometry><cylinder length="0.05" radius="0.04"/></geometry></collision>
  <inertial><mass value="0.2"/><inertia ixx="0.0001" ixy="0.0" ixz="0.0" iyy="0.0001" iyz="0.0" izz="0.0001"/></inertial>
</link>

<!-- 2. 偏航关节(360度旋转) -->
<xacro:pan_joint 
  name="pan_joint" 
  parent_link="base_link" 
  child_link="pan_link" 
  x_offset="0.1" y_offset="0" z_offset="0.1"/>

<!-- 3. 俯仰关节(-45~45度) -->
<xacro:tilt_joint 
  name="tilt_joint" 
  parent_link="pan_link" 
  child_link="camera_link" 
  x_offset="0" y_offset="0" z_offset="0.05"
  lower="${deg2rad(-45)}" upper="${deg2rad(45)}"/>

<!-- 4. 挂载相机 -->
<xacro:camera name="camera_link" parent_link="pan_link" x_offset="0" y_offset="0" z_offset="0" roll="0" pitch="0" yaw="0"/>
模板使用指南
1. 快速复用步骤
  1. 复制所需模块的宏定义到你的 Xacro 文件;
  2. 修改宏调用时的参数(如尺寸、安装位置、质量);
  3. 编译为 URDF 验证:xacro your_robot.xacro > your_robot.urdf
  4. check_urdf 检查语法,RViz2 可视化调试。
2. 模块化管理建议

将不同模块拆分到独立文件,通过 <xacro:include> 组合:

复制代码
urdf/
├── base.xacro       # 底盘
├── wheels.xacro     # 车轮+脚轮
├── sensors/
│   ├── lidar.xacro  # 激光雷达
│   ├── camera.xacro # 相机
│   └── imu.xacro    # IMU
├── arm.xacro        # 机械臂
└── robot.xacro      # 主文件(包含所有模块)
3. 适配 Gazebo 仿真

若需在 Gazebo 中使用,需添加以下扩展属性(示例:车轮):

复制代码
<!-- 在wheel宏的link节点内添加Gazebo属性 -->
<gazebo reference="${wheel_name}">
  <material>Gazebo/Gray</material>
  <turnGravityOff>false</turnGravityOff>
  <rollingFriction>0.01</rollingFriction>
  <slidingFriction>0.01</slidingFriction>
  <mu1>1.0</mu1>
  <mu2>1.0</mu2>
</gazebo>

<!-- 在wheel宏的joint节点内添加Gazebo传动(控制车轮) -->
<gazebo>
  <plugin name="wheel_controller" filename="libgazebo_ros_diff_drive.so">
    <ros>
      <namespace>/</namespace>
      <remapping>cmd_vel:=cmd_vel</remapping>
    </ros>
    <leftJoint>${parent_link}_to_left_wheel</leftJoint>
    <rightJoint>${parent_link}_to_right_wheel</rightJoint>
    <wheelSeparation>${width}</wheelSeparation>
    <wheelDiameter>${2*radius}</wheelDiameter>
    <maxVelocity>1.0</maxVelocity>
    <acceleration>0.5</acceleration>
  </plugin>
</gazebo>
总结
核心模板清单
  1. 底盘 + 车轮:移动机器人基础,适配差速小车;
  2. 激光雷达 + 相机 + IMU:常用传感器,固定关节安装;
  3. 机械臂关节 + 云台:适用于机械臂 / 云台类机器人;
  4. 脚轮:从动轮,适配三轮 / 四轮小车。
关键修改参数
模块 核心修改参数 示例值
底盘 length/width/height/mass 0.3/0.2/0.1/1.5
车轮 radius/width/mass/ 安装偏移 0.06/0.03/0.3
传感器 x_offset/y_offset/z_offset 0.15/0/0.08
关节 lower_limit/upper_limit -180°/180°
相关推荐
yi.Ist2 小时前
关于若干基础的几何问题
c++·学习·算法·计算几何
曦月逸霜3 小时前
深入理解计算机系统——学习笔记(持续更新~)
笔记·学习·计算机系统
我的xiaodoujiao3 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 43--添加allure测试报告显示信息和其他封装方法
python·学习·测试工具·allure
curry____3033 小时前
数据结构学习笔记
数据结构·笔记·学习
宫瑾3 小时前
【C语言】嵌入式C加强学习
java·c语言·学习
LaoZhangGong1234 小时前
学习TCP/IP的第4步:重点掌握TCP序列号和确认号
网络·学习·tcp/ip·以太网
DuHz4 小时前
UWB 雷达综述精读:应用、标准、信号处理、数据集、芯片与未来方向——论文阅读
论文阅读·学习·算法·信息与通信·信号处理
calvinpaean4 小时前
Video Depth Anything: Consistent Depth Estimation for Super-Long Videos论文学习
学习
wubba lubba dub dub7504 小时前
第三十四周 学习周报
学习