ROS2 Humble 完全实战手册:从环境搭建到自主导航 🚀

ROS2 0 次阅读
ROS2 Humble 完全实战手册:从环境搭建到自主导航 🚀

ROS2 Humble 完全实战手册:从环境搭建到自主导航 🚀

一份面向机器人开发者的 ROS2 Humble 全栈指南,涵盖核心概念、节点通信、Gazebo 仿真和 Nav2 导航——读完就能上手搭建你的第一个智能机器人项目。

目录

  1. 为什么选择 ROS2 Humble
  2. ROS2 核心架构解密
  3. 环境搭建与第一个节点
  4. 话题、服务与动作——三大通信范式
  5. Gazebo 仿真实战
  6. Nav2 自主导航从零到跑通
  7. 常见问题与排错指南
  8. 总结与学习路径

一、为什么选择 ROS2 Humble

1.1 ROS1 的遗产与 ROS2 的诞生

2007 年,Willow Garage 发布了 ROS(Robot Operating System),这个"机器人领域的 Linux"迅速成为学术研究和工业原型的事实标准。但 ROS1 诞生于云计算和 5G 尚未普及的年代,它在设计上存在几个天生的短板:

  • 单点故障:依赖中心化 roscore,master 挂了整个系统就瘫痪
  • 非实时:基于 TCP 的通信无法满足硬实时场景
  • 安全性弱:没有原生的加密和认证机制
  • 跨平台困难:深度绑定 Ubuntu,Windows/macOS 支持惨淡

ROS2 从 2017 年 Ardent 版本起步,历经 Crystal、Dashing、Eloquent、Foxy、Galactic 多轮迭代,最终在 2022 年 5 月发布的 Humble Hawksbill 上达到了成熟稳定的里程碑。Humble 是 ROS2 第二个 LTS(长期支持)版本(首个为 Foxy Fitzroy 2020),支持周期到 2027 年 5 月——这意味你现在投入的学习,至少未来四年都不会过时。

1.2 Humble 带来了什么

特性 ROS1 Noetic ROS2 Humble
通信中间件 TCPROS/UDPROS DDS(默认 Fast-DDS)
架构模式 中心化 Master 去中心化 Discovery
实时性 ❌ 不保证 ✅ 支持 RTPS 实时配置
安全性 ✅ DDS-Security 加密
跨平台 Ubuntu only ✅ Ubuntu / Windows / macOS
Python 版本 Python 2/3 混用 Python 3.10+
组网能力 单机为主 ✅ 原生多机分布式
生命周期管理 ✅ Managed Nodes
QoS 策略 ✅ 灵活的 QoS 配置
LTS 支持 到 2025 到 2027 年 5 月
官方仿真 Gazebo Classic ✅ Gazebo (Ignition) Fortress

简单说:如果你现在要从零开始学机器人开发,ROS2 Humble 是唯一正确的起点。

1.3 本文读者收益

读完这篇教程,你将能够:

  • 在 Ubuntu 22.04 上搭建完整的 ROS2 Humble 开发环境
  • 理解 DDS 通信机制,能独立编写 Publisher / Subscriber / Service / Action 节点
  • 使用 Gazebo 仿真环境运行机器人模型
  • 配置并运行 Nav2 导航栈,实现自主路径规划
  • 避开新手最常踩的 10 个坑

二、ROS2 核心架构解密

2.1 去中心化:告别 roscore

ROS2 最大的架构变革是抛弃了中心化的 roscore,转而采用 DDS(Data Distribution Service)的 自动发现机制。每个节点启动时,只需要知道自己在哪个 DDS Domain 里,就可以自动发现同域内的其他节点。

┌──────────────────────────────────────────────────┐
│                  DDS Domain 0                      │
│                                                    │
│  ┌──────────┐    ┌──────────┐    ┌──────────────┐ │
│  │ Node A   │◄──►│ Node B   │◄──►│   Node C     │ │
│  │ Publisher│    │Subscriber│    │Service Server│ │
│  └──────────┘    └──────────┘    └──────────────┘ │
│                                                    │
│  所有节点对等,无需 Master — Discovery 全自动完成    │
└──────────────────────────────────────────────────┘

这种设计带来了三个直接的好处:

  1. 高容错:任何一个节点崩溃,其他节点不受影响
  2. 即插即用:新节点加入网络后自动被发现,无需重启系统
  3. 天然多机:同一 DDS Domain 内,跨机器的节点通信和本机一样简单

2.2 DDS:ROS2 的通信心脏

DDS 是 OMG 组织制定的分布式实时通信标准。ROS2 默认使用 eProsima 的 Fast-DDS(以前叫 Fast-RTPS),但你也可以换成 CycloneDDS 或 RTI Connext。

理解 DDS,只需要抓住三个核心概念:

  • Participant(参与者):对应 ROS2 的 Context / Node,是 DDS 世界里的"公民"
  • Topic(主题):和 ROS1 的 Topic 类似,是数据的逻辑通道
  • DataWriter / DataReader:对应 ROS2 的 Publisher / Subscriber

而 DDS 为 ROS2 带来的杀手级特性是 QoS(Quality of Service),它让你可以精确控制通信行为:

from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy

# 定义一个"可靠 + 持久"的 QoS 配置
qos = QoSProfile(
    reliability=ReliabilityPolicy.RELIABLE,   # 确保送达
    durability=DurabilityPolicy.TRANSIENT_LOCAL, # 后加入的订阅者也能收到历史消息
    history=HistoryPolicy.KEEP_LAST,
    depth=10
)

常见的 QoS 组合场景:

场景 Reliability Durability Depth
传感器数据流(激光雷达) BEST_EFFORT VOLATILE 1-5
关键指令(运动控制) RELIABLE VOLATILE 1
参数/配置 RELIABLE TRANSIENT_LOCAL 10
状态信息(电池、温度) RELIABLE VOLATILE 5

💡 新手提示:ROS2 默认的 QoS 是 KEEP_LAST(10) + RELIABLE + VOLATILE,绝大多数场景不需要修改。只有当你遇到"丢消息"或"延迟太高"时才需要调优。

ROS2 DDS 去中心化架构

2.3 节点生命周期(Managed Nodes)

ROS1 的节点只有"活着"和"死了"两种状态。ROS2 引入了 生命周期节点,一个节点可以经历多个阶段:

Unconfigured → Inactive → Active → (Error) → Finalized

这对于生产环境至关重要——你可以先加载配置、再激活节点,确保启动过程可控。


三、环境搭建与第一个节点

3.1 系统要求与安装

ROS2 Humble 官方支持 Ubuntu 22.04 LTS(Jammy)。让我们从零开始:

# 1. 设置 locale
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8

# 2. 添加 ROS2 仓库
sudo apt install software-properties-common
sudo add-apt-repository universe
sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
  -o /usr/share/keyrings/ros-archive-keyring.gpg

echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
  http://packages.ros.org/ros2/ubuntu \
  $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | \
  sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

# 3. 安装 ROS2 Humble Desktop 完整版
sudo apt update
sudo apt install ros-humble-desktop -y

# 4. 安装开发工具
sudo apt install python3-colcon-common-extensions \
  python3-rosdep python3-vcstool ros-dev-tools -y

# 5. 初始化 rosdep
sudo rosdep init
rosdep update

安装完成后,在 ~/.bashrc 中添加环境变量:

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

验证安装:

# 启动一个 talker 节点
ros2 run demo_nodes_cpp talker

如果能持续输出 Hello World 消息,说明安装成功。

3.2 工作空间与包结构

ROS2 的标准工作空间布局如下:

~/ros2_ws/
├── src/                    # 源码目录
│   ├── my_robot_msgs/      # 自定义消息包
│   │   ├── msg/
│   │   ├── CMakeLists.txt
│   │   └── package.xml
│   ├── my_robot_bringup/   # 启动文件包
│   │   ├── launch/
│   │   └── package.xml
│   └── my_robot_control/   # 控制节点包
│       ├── src/
│       ├── CMakeLists.txt
│       └── package.xml
├── build/                  # 编译中间产物(colcon build 自动生成)
├── install/                # 安装目录(colcon build 自动生成)
└── log/                    # 编译日志

创建一个工作空间:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
colcon build --symlink-install

--symlink-install 参数让你修改 Python 脚本后无需重新编译,开发效率飙升。

3.3 Hello World:第一个节点

~/ros2_ws/src 下创建一个 Python 包:

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python py_hello_world

编辑 ~/ros2_ws/src/py_hello_world/py_hello_world/talker.py

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class HelloTalker(Node):
    """一个简单的发布者节点——每秒发送一条消息"""

    def __init__(self):
        super().__init__('hello_talker')
        self.publisher = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.count = 0
        self.get_logger().info('Hello Talker 节点已启动 🚀')

    def timer_callback(self):
        msg = String()
        msg.data = f'你好,ROS2 世界!这是第 {self.count} 条消息'
        self.publisher.publish(msg)
        self.get_logger().info(f'📤 发布: "{msg.data}"')
        self.count += 1


def main(args=None):
    rclpy.init(args=args)
    node = HelloTalker()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

同时创建 listener.py

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class HelloListener(Node):
    """一个简单的订阅者节点——接收并打印消息"""

    def __init__(self):
        super().__init__('hello_listener')
        self.subscription = self.create_subscription(
            String, 'chatter', self.listener_callback, 10)
        self.get_logger().info('Hello Listener 节点已就绪 👂')

    def listener_callback(self, msg):
        self.get_logger().info(f'📥 收到: "{msg.data}"')


def main(args=None):
    rclpy.init(args=args)
    node = HelloListener()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

setup.py 中注册入口点:

entry_points={
    'console_scripts': [
        'talker = py_hello_world.talker:main',
        'listener = py_hello_world.listener:main',
    ],
},

编译并运行:

cd ~/ros2_ws
colcon build --symlink-install --packages-select py_hello_world
source install/setup.bash

# 终端1:启动 talker
ros2 run py_hello_world talker

# 终端2:启动 listener
ros2 run py_hello_world listener

你应该看到 listener 终端开始接收消息——恭喜,你已经跨过了 ROS2 的第一道门槛!


四、话题、服务与动作——三大通信范式

ROS2 提供了三种标准的节点通信方式,各有各的适用场景:

ROS2 三大通信范式对比

4.1 话题(Topic)——"广播电台"

适用场景:传感器数据流、状态广播、实时控制指令

话题是 ROS2 中最基础的通信方式:一个 Publisher 不断发布消息,任意多个 Subscriber 可以同时接收。它是单向、异步、多对多的。

C++ 版本的 Publisher 示例:

#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"

class LaserPublisher : public rclcpp::Node {
public:
  LaserPublisher() : Node("laser_publisher") {
    publisher_ = this->create_publisher<sensor_msgs::msg::LaserScan>("scan", 10);
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&LaserPublisher::publish_scan, this));
  }

private:
  void publish_scan() {
    auto msg = sensor_msgs::msg::LaserScan();
    msg.header.stamp = this->now();
    msg.header.frame_id = "laser_frame";
    msg.angle_min = -1.57;
    msg.angle_max = 1.57;
    msg.angle_increment = 0.0175;
    msg.range_min = 0.1;
    msg.range_max = 30.0;
    // 填充实际距离数据...
    publisher_->publish(msg);
  }

  rclcpp::Publisher<sensor_msgs::msg::LaserScan>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};

4.2 服务(Service)——"电话呼叫"

适用场景:请求-响应模式,如获取地图、设置参数、触发拍照

服务是同步、一对一的通信:一个 Client 发送请求,等待 Server 处理并返回响应。请求期间 Client 会阻塞(或使用异步回调)。

Python 版 Service Server:

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node


class AdderServer(Node):
    def __init__(self):
        super().__init__('adder_server')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)
        self.get_logger().info('加法服务已就绪 ➕')

    def add_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info(f'计算: {request.a} + {request.b} = {response.sum}')
        return response


def main():
    rclpy.init()
    node = AdderServer()
    rclpy.spin(node)
    rclpy.shutdown()

调用服务:

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 42, b: 58}"

4.3 动作(Action)——"外卖订单"

适用场景:长时间运行的任务,如导航到目标点、抓取物体、机械臂运动

动作是 ROS2 中最复杂也最强大的通信机制。它本质上是一对"话题 + 服务"的组合,包含三个通道:

通道 方向 作用
Goal Client → Server 发送任务目标
Feedback Server → Client 持续反馈进度
Result Server → Client 任务完成后的最终结果

以导航为例,Action Client 发送目标位姿后,可以实时获取剩余距离、预计时间等反馈,还能随时取消任务:

from action_msgs.msg import GoalStatus
from nav2_msgs.action import NavigateToPose
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node


class NavClient(Node):
    def __init__(self):
        super().__init__('nav_client')
        self.action_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')

    def send_goal(self, x, y, yaw):
        goal_msg = NavigateToPose.Goal()
        goal_msg.pose.header.frame_id = 'map'
        goal_msg.pose.pose.position.x = x
        goal_msg.pose.pose.position.y = y
        goal_msg.pose.pose.orientation.z = yaw

        self.action_client.wait_for_server()
        self.send_goal_future = self.action_client.send_goal_async(
            goal_msg, feedback_callback=self.feedback_callback)
        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('目标已接受 ✅')
        self.get_result_future = goal_handle.get_result_async()
        self.get_result_future.add_done_callback(self.get_result_callback)

    def feedback_callback(self, feedback_msg):
        distance = feedback_msg.feedback.distance_remaining
        self.get_logger().info(f'📏 剩余距离: {distance:.2f} m')

    def get_result_callback(self, future):
        status = future.result().status
        if status == GoalStatus.STATUS_SUCCEEDED:
            self.get_logger().info('🎯 导航成功!')
        else:
            self.get_logger().error(f'导航失败,状态码: {status}')

五、Gazebo 仿真实战

5.1 安装 Gazebo Fortress

ROS2 Humble 不再使用 Gazebo Classic,而是搭配新一代的 Gazebo Fortress(也叫 Ignition Gazebo):

# 安装 Gazebo Fortress
sudo apt install ros-humble-ros-gz -y

# 验证安装
gz sim --version

5.2 创建一个带传感器的机器人

我们使用 URDF(Unified Robot Description Format)描述一个差速驱动机器人,搭载激光雷达和摄像头:

<?xml version="1.0"?>
<robot name="my_robot" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- 底盘 -->
  <link name="base_link">
    <visual>
      <geometry>
        <box size="0.4 0.3 0.15"/>
      </geometry>
      <material name="blue">
        <color rgba="0.2 0.4 0.8 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <box size="0.4 0.3 0.15"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="5.0"/>
      <inertia ixx="0.1" ixy="0.0" ixz="0.0"
               iyy="0.1" iyz="0.0" izz="0.2"/>
    </inertial>
  </link>

  <!-- 左轮 -->
  <link name="left_wheel">
    <visual>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </geometry>
    </visual>
    <collision>
      <geometry>
        <cylinder radius="0.05" length="0.03"/>
      </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>

  <joint name="left_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="left_wheel"/>
    <origin xyz="-0.15 0.18 -0.05" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>

  <!-- 激光雷达 -->
  <link name="laser_frame">
    <visual>
      <geometry>
        <cylinder radius="0.03" length="0.05"/>
      </geometry>
    </visual>
  </link>

  <joint name="laser_joint" type="fixed">
    <parent link="base_link"/>
    <child link="laser_frame"/>
    <origin xyz="0.15 0 0.1" rpy="0 0 0"/>
  </joint>

  <!-- Gazebo 激光雷达插件 -->
  <gazebo reference="laser_frame">
    <sensor type="gpu_lidar" name="lidar_sensor">
      <update_rate>10</update_rate>
      <ray>
        <scan>
          <horizontal>
            <samples>360</samples>
            <resolution>1</resolution>
            <min_angle>-3.14159</min_angle>
            <max_angle>3.14159</max_angle>
          </horizontal>
        </scan>
        <range>
          <min>0.05</min>
          <max>15.0</max>
          <resolution>0.01</resolution>
        </range>
      </ray>
      <plugin
        filename="libgazebo_ros_lidar_sensor.so"
        name="lidar_controller">
        <ros>
          <namespace>/my_robot</namespace>
        </ros>
        <topic>scan</topic>
        <frame_name>laser_frame</frame_name>
      </plugin>
    </sensor>
  </gazebo>

  <!-- 差速驱动插件 -->
  <gazebo>
    <plugin
      filename="libgazebo_ros_diff_drive.so"
      name="diff_drive_controller">
      <ros>
        <namespace>/my_robot</namespace>
      </ros>
      <left_joint>left_wheel_joint</left_joint>
      <right_joint>right_wheel_joint</right_joint>
      <wheel_separation>0.36</wheel_separation>
      <wheel_radius>0.05</wheel_radius>
      <command_topic>cmd_vel</command_topic>
      <odometry_topic>odom</odometry_topic>
    </plugin>
  </gazebo>
</robot>

5.3 在 Gazebo 中启动机器人

编写 launch 文件 ~/ros2_ws/src/my_robot_bringup/launch/sim.launch.py

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import ExecuteProcess


def generate_launch_description():
    return LaunchDescription([
        # 启动 Gazebo Fortress
        ExecuteProcess(
            cmd=['gz', 'sim', 'empty.sdf', '-r'],
            output='screen'
        ),

        # 生成机器人
        Node(
            package='ros_gz_sim',
            executable='create',
            arguments=[
                '-name', 'my_robot',
                '-topic', 'robot_description',
                '-x', '0.0', '-y', '0.0', '-z', '0.1'
            ],
            output='screen'
        ),

        # 启动 robot_state_publisher
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            parameters=[{'robot_description': '...'}],
            output='screen'
        ),

        # 启动 RViz2
        Node(
            package='rviz2',
            executable='rviz2',
            output='screen'
        )
    ])

启动仿真:

ros2 launch my_robot_bringup sim.launch.py

你会看到 Gazebo 窗口中出现一个蓝色小机器人,用键盘或手柄可以控制它在虚拟世界中移动——仿真世界的大门已经打开!


六、Nav2 自主导航从零到跑通

6.1 Nav2 架构速览

Nav2(Navigation2)是 ROS2 生态中最成熟的导航框架,它把导航任务拆解成多个独立节点,各司其职:

Nav2 导航栈架构

核心模块:

模块 作用 关键节点
Planner Server 全局路径规划(A* / Smac / NavFn) planner_server
Controller Server 局部轨迹跟踪(DWB / MPPI / RPP) controller_server
Behavior Tree 任务编排与状态机管理 bt_navigator
AMCL 蒙特卡洛定位 amcl
Costmap 2D 障碍物代价地图生成 global_costmap / local_costmap
Smoother Server 路径平滑优化 smoother_server
Waypoint Follower 多点导航 waypoint_follower

6.2 安装与配置

sudo apt install ros-humble-navigation2 ros-humble-nav2-bringup -y

创建一个导航配置包,核心是 nav2_params.yaml

amcl:
  ros__parameters:
    alpha1: 0.2
    alpha2: 0.2
    alpha3: 0.2
    alpha4: 0.2
    base_frame_id: "base_link"
    global_frame_id: "map"
    odom_frame_id: "odom"
    robot_model_type: "differential"
    laser_model_type: "likelihood_field"
    laser_max_range: 15.0
    laser_min_range: 0.05
    max_particles: 2000
    min_particles: 500
    update_min_d: 0.1
    update_min_a: 0.1

bt_navigator:
  ros__parameters:
    global_frame: map
    robot_base_frame: base_link
    default_bt_xml_filename: "navigate_to_pose_w_replanning_and_recovery.xml"

planner_server:
  ros__parameters:
    expected_planner_frequency: 1.0
    planner_plugins: ["GridBased"]
    GridBased:
      plugin: "nav2_smac_planner/SmacPlannerHybrid"
      tolerance: 0.25
      downsample_costmap: false
      angle_quantization_bins: 72

controller_server:
  ros__parameters:
    controller_plugins: ["FollowPath"]
    FollowPath:
      plugin: "dwb_core::DWBLocalPlanner"
      debug_trajectory_details: true
      min_vel_x: -0.1
      max_vel_x: 0.5
      max_vel_theta: 1.5
      min_speed_xy: 0.0
      max_speed_xy: 0.5
      critics: ["RotateToGoal", "Oscillation", "BaseObstacle", "GoalAlign",
                "PathAlign", "PathDist", "GoalDist"]
      PathDist.scale: 32.0
      GoalDist.scale: 24.0

local_costmap:
  local_costmap:
    ros__parameters:
      update_frequency: 5.0
      publish_frequency: 2.0
      global_frame: odom
      robot_base_frame: base_link
      robot_radius: 0.22
      resolution: 0.05
      width: 3
      height: 3
      plugins: ["obstacle_layer", "inflation_layer"]
      inflation_layer:
        inflation_radius: 0.35
      obstacle_layer:
        observation_sources: "scan"
        scan:
          topic: /scan
          max_obstacle_height: 2.0
          marking: true
          clearing: true

6.3 启动导航并发送目标

# 启动完整的 Nav2 导航栈
ros2 launch nav2_bringup navigation_launch.py \
  params_file:=./config/nav2_params.yaml \
  use_sim_time:=true

# 启动 RViz 并发送导航目标
ros2 launch nav2_bringup rviz_launch.py

在 RViz 中点击 "2D Goal Pose" 按钮,在地图上点击一个目标点,你会看到:

  1. 绿色线条——Planner 生成的全局路径
  2. 红色短线——Controller 实时规划的局部轨迹
  3. 机器人开始移动,最终停在目标点附近

6.4 编程发送导航目标

#!/usr/bin/env python3
import math
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped
from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient


class WaypointNavigator(Node):
    """按顺序导航到多个目标点"""

    def __init__(self):
        super().__init__('waypoint_navigator')
        self.client = ActionClient(self, NavigateToPose, 'navigate_to_pose')

        # 定义巡逻点(x, y, yaw)
        self.waypoints = [
            (2.0, 1.0, 0.0),
            (4.0, 3.0, math.pi/2),
            (1.0, 4.0, math.pi),
            (0.0, 0.0, 0.0),
        ]
        self.current_wp = 0

    def send_next_goal(self):
        if self.current_wp >= len(self.waypoints):
            self.get_logger().info('🏁 所有巡逻点已完成!')
            return

        x, y, yaw = self.waypoints[self.current_wp]
        goal = NavigateToPose.Goal()
        goal.pose.header.frame_id = 'map'
        goal.pose.pose.position.x = x
        goal.pose.pose.position.y = y
        goal.pose.pose.orientation.z = math.sin(yaw / 2)
        goal.pose.pose.orientation.w = math.cos(yaw / 2)

        self.get_logger().info(f'📍 发送目标 {self.current_wp+1}/{len(self.waypoints)}: ({x}, {y})')
        self.client.wait_for_server()
        future = self.client.send_goal_async(goal)
        future.add_done_callback(self.goal_done)

    def goal_done(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().error('目标被拒绝')
            return
        result_future = goal_handle.get_result_async()
        result_future.add_done_callback(self.result_done)

    def result_done(self, future):
        self.current_wp += 1
        self.send_next_goal()


def main():
    rclpy.init()
    navigator = WaypointNavigator()
    navigator.send_next_goal()
    rclpy.spin(navigator)

七、常见问题与排错指南

Q1:ros2 命令找不到?

# 确认 source 了 setup 文件
source /opt/ros/humble/setup.bash
# 检查是否能找到 ros2
which ros2  # 应输出 /opt/ros/humble/bin/ros2

Q2:节点间无法通信?

使用 ROS2 自带的诊断工具:

# 检查话题列表
ros2 topic list

# 检查节点图
ros2 node list
rqt_graph  # 可视化节点连接关系

# 检查话题信息
ros2 topic info /chatter

# 使用 echo 查看实际消息
ros2 topic echo /chatter

Q3:Gazebo 仿真启动后机器人不动?

检查 /cmd_vel 话题是否正确连接:

ros2 topic echo /cmd_vel
# 手动发送控制指令测试
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.2}, angular: {z: 0.0}}"

Q4:Nav2 导航时机器人疯狂转圈?

通常是 Costmap 参数不合理导致的。检查:

  • robot_radius 是否和实际机器人尺寸匹配
  • inflation_radius 不应过大(建议 0.3-0.5m)
  • 激光雷达的 min_range 是否设得过大,导致近距离障碍物不被检测

Q5:DDS 发现不到其他机器上的节点?

# 检查 DDS 配置
echo $ROS_DOMAIN_ID  # 所有机器必须相同

# 如果使用 Fast-DDS,检查多播是否正常
# 在 /etc/hosts 中添加对方 IP
# 或配置 CycloneDDS 的 XML 配置文件

Q6:编译时 ament_cmake 报错?

# 清理 build 目录后重新编译
rm -rf ~/ros2_ws/build ~/ros2_ws/install
colcon build --symlink-install

Q7:Python 节点 import 报错?

确保 setup.py 中的 entry_pointspackage.xml 中的 <exec_depend> 一致,并且:

# 重新 source 安装目录
source ~/ros2_ws/install/setup.bash
# 检查包是否被正确发现
ros2 pkg list | grep my_package

八、总结与学习路径

核心要点回顾

  1. ROS2 Humble = 未来四年的标准:作为 ROS2 第二个 LTS 版本(首个为 Foxy Fitzroy 2020),它是学习和生产的唯一选择
  2. DDS 是灵魂:去中心化 + QoS 让 ROS2 在可靠性、实时性、安全性上全面超越 ROS1
  3. 三大通信范式:Topic(数据流)、Service(请求响应)、Action(长任务)覆盖所有场景
  4. 仿真先行:Gazebo + RViz2 组合让你无需硬件就能开发和测试
  5. Nav2 开箱即用:只要配好参数,复杂导航功能一键启动

推荐学习路径

📅 第 1 周:安装 + Hello World + Topic/Service/Action 练习
📅 第 2 周:URDF 建模 + Gazebo 仿真 + 键盘遥控
📅 第 3 周:TF2 坐标变换 + 传感器数据处理
📅 第 4 周:Nav2 导航栈 + SLAM 建图
📅 第 5-6 周:完整项目实战(如仓储巡检机器人)

延伸资源


本文由 MarkShareX AI 自动创作,分类:ROS2,方向:ROS2 Humble